diff --git a/client/src/App.svelte b/client/src/App.svelte
index 1992d9b..b167fae 100644
--- a/client/src/App.svelte
+++ b/client/src/App.svelte
@@ -9,6 +9,7 @@
import Trash from "./routes/Trash.svelte";
import Movies from "./routes/Movies.svelte";
import TvShows from "./routes/TvShows.svelte";
+ import Anime from "./routes/Anime.svelte";
import Music from "./routes/Music.svelte";
import Profile from "./routes/Profile.svelte";
import Settings from "./routes/Settings.svelte";
@@ -16,6 +17,7 @@
import { API, getAccessToken } from "./utils/api.js";
import { refreshMovieCount } from "./stores/movieStore.js";
import { refreshTvShowCount } from "./stores/tvStore.js";
+ import { refreshAnimeCount } from "./stores/animeStore.js";
import { refreshMusicCount } from "./stores/musicStore.js";
import { fetchTrashItems } from "./stores/trashStore.js";
import { setAvatarUrl } from "./stores/avatarStore.js";
@@ -34,6 +36,7 @@
await Promise.all([
refreshMovieCount(),
refreshTvShowCount(),
+ refreshAnimeCount(),
refreshMusicCount(),
fetchTrashItems()
]);
@@ -85,6 +88,7 @@
if (token) {
refreshMovieCount();
refreshTvShowCount();
+ refreshAnimeCount();
refreshMusicCount();
fetchTrashItems();
loadUserProfile();
@@ -150,6 +154,7 @@
+
diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte
index d2f1469..2f5c1f4 100644
--- a/client/src/components/Sidebar.svelte
+++ b/client/src/components/Sidebar.svelte
@@ -3,6 +3,7 @@
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
import { movieCount } from "../stores/movieStore.js";
import { tvShowCount } from "../stores/tvStore.js";
+import { animeCount } from "../stores/animeStore.js";
import { musicCount } from "../stores/musicStore.js";
import { trashCount } from "../stores/trashStore.js";
import { apiFetch } from "../utils/api.js";
@@ -11,6 +12,7 @@ import { musicCount } from "../stores/musicStore.js";
const dispatch = createEventDispatcher();
let hasMovies = false;
let hasShows = false;
+let hasAnime = false;
let hasTrash = false;
let hasMusic = false;
// Svelte store kullanarak reaktivite sağla
@@ -18,7 +20,7 @@ let hasMusic = false;
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
let diskSpace;
let hasMedia = false;
- $: hasMedia = hasMovies || hasShows || hasMusic;
+ $: hasMedia = hasMovies || hasShows || hasAnime || hasMusic;
// Store subscription'ı temizlemek için
let unsubscribeDiskSpace;
@@ -41,6 +43,10 @@ let hasMusic = false;
const unsubscribeTv = tvShowCount.subscribe((count) => {
hasShows = (count ?? 0) > 0;
});
+
+const unsubscribeAnime = animeCount.subscribe((count) => {
+ hasAnime = (count ?? 0) > 0;
+});
const unsubscribeTrash = trashCount.subscribe((count) => {
hasTrash = (count ?? 0) > 0;
@@ -53,6 +59,7 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
onDestroy(() => {
unsubscribeMovie();
unsubscribeTv();
+ unsubscribeAnime();
unsubscribeTrash();
unsubscribeMusic();
if (unsubscribeDiskSpace) {
@@ -192,6 +199,20 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
{/if}
+ {#if hasAnime}
+ ({
+ class: isCurrent ? "item active" : "item",
+ })}
+ on:click={handleLinkClick}
+ >
+
+ Anime
+
+ {/if}
+
{#if hasMusic}
+ import { onMount, tick } from "svelte";
+ import { API, apiFetch } from "../utils/api.js";
+ import { cleanFileName } from "../utils/filename.js";
+ import { animeCount } from "../stores/animeStore.js";
+ import {
+ activeSearchTerm,
+ setSearchScope
+ } from "../stores/searchStore.js";
+
+ let shows = [];
+ let loading = true;
+ let refreshing = false;
+ let rescanning = false;
+ let error = null;
+
+ let selectedShow = null;
+ let selectedSeason = null;
+ let selectedEpisode = null;
+
+let playerItems = [];
+let showPlayerModal = false;
+let selectedVideo = null;
+let videoEl;
+let isPlaying = false;
+let currentTime = 0;
+let duration = 0;
+let volume = 1;
+let subtitleURL = null;
+let subtitleLabel = "Custom Subtitles";
+let currentIndex = -1;
+let seasonPlaylist = [];
+let seasonPlaylistIndex = -1;
+let canPlayPrev = false;
+let canPlayNext = false;
+let searchTerm = "";
+let hasSearch = false;
+let filteredShows = [];
+
+ function runtimeToText(runtime) {
+ if (!runtime || Number.isNaN(runtime)) return null;
+ const minutes = Math.round(Number(runtime));
+ const hours = Math.floor(minutes / 60);
+ const remain = minutes % 60;
+ if (hours && remain) return `${hours}h ${remain}m`;
+ if (hours) return `${hours}h`;
+ return `${remain}m`;
+ }
+
+ function formatVideoInfo(video) {
+ if (!video) return null;
+ const codec = video.codec ? video.codec.toUpperCase() : null;
+ const resolution =
+ video.resolution || (video.height ? `${video.height}p` : null);
+ const hdr = video.colorSpace?.toUpperCase().includes("HDR")
+ ? "HDR"
+ : null;
+ return [codec, resolution, hdr].filter(Boolean).join(" • ") || null;
+ }
+
+ function formatAudioInfo(audio) {
+ if (!audio) return null;
+ const codec = audio.codec ? audio.codec.toUpperCase() : null;
+ let channels = null;
+ if (audio.channelLayout) channels = audio.channelLayout.toUpperCase();
+ else if (audio.channels) {
+ channels =
+ audio.channels === 6
+ ? "5.1"
+ : audio.channels === 2
+ ? "2.0"
+ : `${audio.channels}`;
+ }
+ return [codec, channels].filter(Boolean).join(" • ") || null;
+ }
+
+ function assetUrl(pathname) {
+ if (!pathname) return null;
+ const token = localStorage.getItem("token");
+ return `${API}${pathname}?token=${token}`;
+ }
+
+ function posterUrl(show) {
+ return assetUrl(show?.poster);
+ }
+
+ function backdropUrl(show) {
+ return assetUrl(show?.backdrop);
+ }
+
+ function stillUrl(episode) {
+ return assetUrl(episode?.still);
+ }
+
+ function formatAirDate(value) {
+ if (!value) return null;
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return null;
+ return date.toLocaleDateString();
+ }
+
+ function formatEpisodeCode(episode) {
+ if (!episode) return "";
+ if (episode.code) return episode.code;
+ const season = String(episode.seasonNumber || 0).padStart(2, "0");
+ const ep = String(episode.episodeNumber || 0).padStart(2, "0");
+ return `S${season}E${ep}`;
+ }
+
+ function updateSeasonPlaylist() {
+ if (!selectedVideo || !playerItems.length) {
+ seasonPlaylist = [];
+ seasonPlaylistIndex = -1;
+ canPlayPrev = false;
+ canPlayNext = false;
+ return;
+ }
+ const currentSeason = selectedVideo.episode?.seasonNumber;
+ const showId = selectedVideo.show?.id || selectedVideo.show?.title;
+ seasonPlaylist = playerItems.filter(
+ (item) =>
+ (item.show?.id || item.show?.title) === showId &&
+ item.episode?.seasonNumber === currentSeason
+ );
+ seasonPlaylistIndex = seasonPlaylist.findIndex(
+ (item) =>
+ item.episode?.videoPath === selectedVideo.episode?.videoPath &&
+ (item.show?.id || item.show?.title) === showId
+ );
+ canPlayPrev = seasonPlaylistIndex > 0;
+ canPlayNext =
+ seasonPlaylistIndex !== -1 &&
+ seasonPlaylistIndex < seasonPlaylist.length - 1;
+ }
+
+ function normalizeShow(show) {
+ const seasons = Array.isArray(show.seasons)
+ ? show.seasons
+ .map((season) => {
+ const seasonNumber = Number(
+ season.seasonNumber ?? season.number ?? 0
+ );
+ const name = season.name || `Season ${seasonNumber}`;
+ const episodes = Array.isArray(season.episodes)
+ ? season.episodes
+ .map((episode) => {
+ const episodeNumber = Number(
+ episode.episodeNumber ?? episode.number ?? 0
+ );
+ const seasonNo =
+ Number(episode.seasonNumber ?? seasonNumber) || seasonNumber;
+ const videoPath = episode.videoPath
+ ? episode.videoPath.replace(/\\/g, "/")
+ : episode.file
+ ? `${show.folder}/${episode.file}`.replace(/\\/g, "/")
+ : null;
+ return {
+ ...episode,
+ episodeNumber,
+ seasonNumber: seasonNo,
+ videoPath
+ };
+ })
+ .filter((episode) => episode.videoPath)
+ .sort((a, b) => a.episodeNumber - b.episodeNumber)
+ : [];
+ return {
+ ...season,
+ seasonNumber,
+ name,
+ episodes
+ };
+ })
+ .filter((season) => season.episodes.length)
+ .sort((a, b) => a.seasonNumber - b.seasonNumber)
+ : [];
+ return {
+ ...show,
+ seasons
+ };
+ }
+
+ async function loadShows() {
+ loading = true;
+ error = null;
+ try {
+ const resp = await apiFetch("/api/anime");
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ const list = await resp.json();
+ shows = Array.isArray(list) ? list.map(normalizeShow) : [];
+ animeCount.set(shows.length);
+ } catch (err) {
+ error = err?.message || "Anime alınamadı.";
+ shows = [];
+ animeCount.set(0);
+ } finally {
+ loading = false;
+ }
+ }
+
+ $: searchTerm = $activeSearchTerm;
+ $: hasSearch = searchTerm.trim().length > 0;
+ $: filteredShows = (() => {
+ const query = searchTerm.trim().toLowerCase();
+ if (!query) return shows;
+ return shows.filter((show) => {
+ const fields = [
+ show.title,
+ show.originalTitle,
+ show.metadata?.matched_title,
+ show.metadata?.original_name,
+ show.folder
+ ];
+ return fields
+ .filter(Boolean)
+ .some((value) => String(value).toLowerCase().includes(query));
+ });
+ })();
+
+ async function refreshShows() {
+ try {
+ refreshing = true;
+ const resp = await apiFetch("/api/anime/refresh", { method: "POST" });
+ if (!resp.ok) {
+ const data = await resp.json().catch(() => ({}));
+ throw new Error(data?.error || `HTTP ${resp.status}`);
+ }
+ await loadShows();
+ } catch (err) {
+ console.error("TV metadata refresh error:", err);
+ error = err?.message || "Metadata yenilenemedi.";
+ } finally {
+ refreshing = false;
+ }
+ }
+
+ async function rescanShows() {
+ try {
+ rescanning = true;
+ const resp = await apiFetch("/api/anime/rescan", { method: "POST" });
+ if (!resp.ok) {
+ const data = await resp.json().catch(() => ({}));
+ throw new Error(data?.error || `HTTP ${resp.status}`);
+ }
+ await loadShows();
+ } catch (err) {
+ console.error("TV metadata rescan error:", err);
+ error = err?.message || "Tam tarama sırasında bir sorun oluştu.";
+ } finally {
+ rescanning = false;
+ }
+ }
+
+ function openShow(show) {
+ if (!show) return;
+ selectedShow = show;
+ selectedSeason = show.seasons?.[0] || null;
+ selectedEpisode = selectedSeason?.episodes?.[0] || null;
+ }
+
+ function closeShow() {
+ selectedShow = null;
+ selectedSeason = null;
+ selectedEpisode = null;
+ }
+
+ function selectSeasonNumber(seasonNumber) {
+ if (!selectedShow) return;
+ const season = selectedShow.seasons.find(
+ (s) => s.seasonNumber === seasonNumber
+ );
+ if (!season) return;
+ selectedSeason = season;
+ selectedEpisode = season.episodes?.[0] || null;
+ }
+
+ function selectEpisode(episode) {
+ if (!episode) return;
+ selectedEpisode = episode;
+ }
+
+ function episodeRuntime(episode) {
+ if (!episode) return null;
+ if (episode.runtime) return runtimeToText(episode.runtime);
+ const seconds = Number(episode.mediaInfo?.format?.duration);
+ if (seconds) return runtimeToText(Math.round(seconds / 60));
+ return null;
+ }
+
+ $: selectedRuntime = episodeRuntime(selectedEpisode);
+ $: selectedVideoInfo = selectedEpisode?.mediaInfo
+ ? formatVideoInfo(selectedEpisode.mediaInfo.video)
+ : null;
+ $: selectedAudioInfo = selectedEpisode?.mediaInfo
+ ? formatAudioInfo(selectedEpisode.mediaInfo.audio)
+ : null;
+ $: selectedAirDate = formatAirDate(selectedEpisode?.aired);
+
+ function mapEpisodeToPlayerItem(show, episode) {
+ if (!show || !episode?.videoPath) return null;
+ const name = (episode.videoPath || "").replace(/^\/+/, "");
+ const ext = name.split(".").pop()?.toLowerCase() || "";
+ const inferredType = ext ? `video/${ext}` : "video/mp4";
+ const size =
+ Number(episode.fileSize) ||
+ Number(episode.mediaInfo?.format?.size) ||
+ null;
+ return {
+ name,
+ type: inferredType.startsWith("video/") ? inferredType : "video/mp4",
+ size,
+ show,
+ episode
+ };
+ }
+
+ $: playerItems = shows
+ .flatMap((show) =>
+ show.seasons.flatMap((season) =>
+ season.episodes.map((episode) => mapEpisodeToPlayerItem(show, episode))
+ )
+ )
+ .filter(Boolean);
+
+ const encodePathSegments = (value) =>
+ value
+ ? value
+ .split(/[\\/]/)
+ .map((segment) => encodeURIComponent(segment))
+ .join("/")
+ : "";
+
+ $: selectedName = selectedVideo?.name ?? "";
+ $: encName = encodePathSegments(selectedName);
+ $: downloadHref = encName
+ ? `${API}/downloads/${encName}?token=${localStorage.getItem("token") || ""}`
+ : "#";
+$: selectedLabel = selectedVideo?.episode
+ ? `${selectedVideo.show.title} · ${formatEpisodeCode(
+ selectedVideo.episode
+ )}${
+ selectedVideo.episode.title
+ ? ` — ${selectedVideo.episode.title}`
+ : ""
+ }`
+ : cleanFileName(selectedName);
+
+ $: if (showPlayerModal && selectedVideo) {
+ const idx = playerItems.findIndex(
+ (item) =>
+ item.episode.videoPath === selectedVideo.episode.videoPath
+ );
+ if (idx === -1) {
+ closePlayer();
+ } else if (idx !== currentIndex) {
+ currentIndex = idx;
+ selectedVideo = playerItems[idx];
+ }
+ updateSeasonPlaylist();
+ }
+
+ async function handlePlayEpisode(episode) {
+ if (!episode) return;
+ await openPlayerForEpisode(episode);
+ }
+
+ async function openPlayerForEpisode(episode) {
+ if (!playerItems.length || !episode?.videoPath) return;
+ const idx = playerItems.findIndex(
+ (item) => item.episode.videoPath === episode.videoPath
+ );
+ if (idx === -1) return;
+ await openVideoAtIndex(idx);
+ }
+
+async function openVideoAtIndex(index) {
+ if (!playerItems.length) return;
+ const safeIndex =
+ ((index % playerItems.length) + playerItems.length) %
+ playerItems.length;
+ stopCurrentVideo();
+ currentIndex = safeIndex;
+ selectedVideo = playerItems[safeIndex];
+ subtitleURL = null;
+ await tick();
+ showPlayerModal = true;
+ isPlaying = false;
+ currentTime = 0;
+ duration = 0;
+ updateSeasonPlaylist();
+
+ if (selectedShow && selectedVideo) {
+ const targetSeason = selectedShow.seasons?.find(
+ (season) =>
+ season.seasonNumber === selectedVideo.episode?.seasonNumber
+ );
+ if (targetSeason) {
+ const targetEpisode = targetSeason.episodes?.find(
+ (episode) =>
+ episode.videoPath === selectedVideo.episode?.videoPath
+ );
+ if (targetEpisode) {
+ selectedSeason = targetSeason;
+ selectedEpisode = targetEpisode;
+ }
+ }
+ }
+
+ // Video element'in yüklendiğinden emin ol
+ await tick();
+ setTimeout(() => {
+ if (videoEl) {
+ console.log("Video element found after timeout:", videoEl);
+ console.log("Video src:", videoEl.src);
+ console.log("Video readyState:", videoEl.readyState);
+ } else {
+ console.error("Video element not found after timeout");
+ }
+ }, 1000);
+}
+
+ function stopCurrentVideo() {
+ if (videoEl) {
+ try {
+ videoEl.pause();
+ videoEl.src = "";
+ videoEl.load();
+ } catch (err) {
+ console.warn("Video stop error:", err?.message || err);
+ }
+ }
+ }
+
+ function closePlayer() {
+ stopCurrentVideo();
+ showPlayerModal = false;
+ selectedVideo = null;
+ subtitleURL = null;
+ isPlaying = false;
+ seasonPlaylist = [];
+ seasonPlaylistIndex = -1;
+ canPlayPrev = false;
+ canPlayNext = false;
+ }
+
+ function getVideoURL() {
+ if (!selectedName) return "";
+ const token = localStorage.getItem("token");
+ const encoded = encodePathSegments(selectedName);
+ if (!encoded) return "";
+ return `${API}/media/${encoded}?token=${token}`;
+ }
+
+ function playEpisodeFromCard(episode) {
+ if (!episode) return;
+ selectEpisode(episode);
+ handlePlayEpisode(episode);
+ }
+
+ function openSeasonNeighbor(offset) {
+ updateSeasonPlaylist();
+ if (!seasonPlaylist.length) return;
+ const target = seasonPlaylist[seasonPlaylistIndex + offset];
+ if (!target) return;
+ const idx = playerItems.findIndex(
+ (item) =>
+ item.episode?.videoPath === target.episode?.videoPath &&
+ (item.show?.id || item.show?.title) ===
+ (target.show?.id || target.show?.title)
+ );
+ if (idx !== -1) {
+ openVideoAtIndex(idx);
+ }
+ }
+
+ function playPrevEpisode() {
+ if (!canPlayPrev) return;
+ openSeasonNeighbor(-1);
+ }
+
+ function playNextEpisode() {
+ if (!canPlayNext) return;
+ openSeasonNeighbor(1);
+ }
+
+ function handleSeasonButtonKey(event, seasonNumber) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ selectSeasonNumber(seasonNumber);
+ }
+ }
+
+ async function togglePlay() {
+ if (!videoEl) return;
+ if (videoEl.paused) {
+ try {
+ await videoEl.play();
+ isPlaying = true;
+ } catch (err) {
+ console.warn("Play rejected:", err?.message || err);
+ isPlaying = false;
+ }
+ } else {
+ videoEl.pause();
+ isPlaying = false;
+ }
+ }
+
+ function updateProgress() {
+ currentTime = videoEl?.currentTime || 0;
+ }
+
+ function updateDuration() {
+ duration = videoEl?.duration || 0;
+ }
+
+ function seekVideo(event) {
+ if (!videoEl) return;
+ const value = Number(event.target.value);
+ videoEl.currentTime = value;
+ currentTime = value;
+ }
+
+ function changeVolume(event) {
+ const value = Number(event.target.value);
+ volume = value;
+ if (videoEl) videoEl.volume = value;
+ event.target.style.setProperty("--fill", value * 100);
+ }
+
+ function toggleFullscreen() {
+ if (!videoEl) return;
+ if (document.fullscreenElement) document.exitFullscreen();
+ else videoEl.requestFullscreen();
+ }
+
+ function formatSize(bytes) {
+ if (!bytes) return "0 MB";
+ if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
+ if (bytes < 1e9) return (bytes / 1e6).toFixed(1) + " MB";
+ return (bytes / 1e9).toFixed(2) + " GB";
+ }
+
+ function formatTime(seconds) {
+ const m = Math.floor(seconds / 60)
+ .toString()
+ .padStart(2, "0");
+ const s = Math.floor(seconds % 60)
+ .toString()
+ .padStart(2, "0");
+ return `${m}:${s}`;
+ }
+
+ function handleSubtitleUpload(event) {
+ const file = event.target.files?.[0];
+ if (!file) return;
+ const ext = file.name.split(".").pop().toLowerCase();
+ const reader = new FileReader();
+ reader.onload = (ev) => {
+ const decoder = new TextDecoder("utf-8");
+ const content =
+ typeof ev.target.result === "string"
+ ? ev.target.result
+ : decoder.decode(ev.target.result);
+ if (ext === "srt") {
+ const vttText =
+ "\uFEFFWEBVTT\n\n" + content.replace(/\r+/g, "").replace(/,/g, ".");
+ const blob = new Blob([vttText], { type: "text/vtt;charset=utf-8" });
+ subtitleURL = URL.createObjectURL(blob);
+ } else if (ext === "vtt") {
+ const blob = new Blob([content], { type: "text/vtt;charset=utf-8" });
+ subtitleURL = URL.createObjectURL(blob);
+ } else {
+ alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
+ }
+ };
+ reader.readAsArrayBuffer(file);
+ }
+
+ const TEXT_INPUT_TYPES = new Set([
+ "text",
+ "search",
+ "email",
+ "password",
+ "number",
+ "url",
+ "tel"
+ ]);
+
+ function isEditableTarget(target) {
+ if (!target) return false;
+ const tag = target.tagName;
+ const type = target.type?.toLowerCase();
+ if (tag === "TEXTAREA") return true;
+ if (tag === "INPUT" && TEXT_INPUT_TYPES.has(type)) return true;
+ return Boolean(target.isContentEditable);
+ }
+
+ $: if (videoEl) {
+ videoEl.volume = volume;
+ }
+
+ onMount(() => {
+ setSearchScope("anime");
+ loadShows();
+ const handleKey = (event) => {
+ if (!showPlayerModal) return;
+ if (isEditableTarget(event.target)) return;
+
+ const isCmd = event.metaKey || event.ctrlKey;
+ if (isCmd && event.key.toLowerCase() === "a") {
+ // Text input'larda çalıştırma
+ if (isEditableTarget(event.target)) return;
+ event.preventDefault();
+ // TvShows sayfasında tümünü seçme işlevi yok, sadece engelleme yapıyoruz
+ return;
+ }
+
+ if (event.key === "Escape") {
+ event.preventDefault();
+ closePlayer();
+ } else if (event.key === " " || event.key === "Spacebar") {
+ event.preventDefault();
+ togglePlay();
+ }
+ };
+ window.addEventListener("keydown", handleKey);
+ return () => window.removeEventListener("keydown", handleKey);
+ });
+
+
+
+
+
+
+ {#if loading}
+ Loading shows…
+ {:else if error}
+ {error}
+ {:else if shows.length === 0}
+ No TV metadata found yet.
+ {:else if hasSearch && filteredShows.length === 0}
+ Aramanıza uyan dizi bulunamadı.
+ {:else}
+
+ {#each filteredShows as show}
+
openShow(show)}>
+ {#if show.poster}
+
+
})
+
+ {:else}
+
+
+
+ {/if}
+
+
+ {/each}
+
+ {/if}
+
+
+{#if selectedShow}
+
+
+
+
+
+
+ {#if selectedShow.poster}
+
})
+ {:else}
+
+
+
+ {/if}
+
+
+
{selectedShow.title}
+
+ {#if selectedShow.year}
+ {selectedShow.year}
+ {/if}
+ {#if selectedShow.status}
+ • {selectedShow.status}
+ {/if}
+ {#if selectedSeason}
+
+ •
+ {selectedSeason.name || `Season ${selectedSeason.seasonNumber}`}
+
+ {/if}
+
+ {#if selectedShow.genres?.length}
+
+ {selectedShow.genres.join(" • ")}
+
+ {/if}
+
+ {selectedShow.overview || "No synopsis found."}
+
+ {#if selectedRuntime || selectedVideoInfo || selectedAudioInfo || selectedAirDate}
+
+ {#if selectedRuntime}
+
+
+ {selectedRuntime}
+
+ {/if}
+ {#if selectedAirDate}
+
+
+ {selectedAirDate}
+
+ {/if}
+ {#if selectedVideoInfo}
+
+
+ {selectedVideoInfo}
+
+ {/if}
+ {#if selectedAudioInfo}
+
+
+ {selectedAudioInfo}
+
+ {/if}
+
+ {/if}
+
+
+
+
+ {#if selectedShow.seasons?.length}
+
+ {#each selectedShow.seasons as season}
+
+ {/each}
+
+ {/if}
+
+
+ {#if selectedSeason?.episodes?.length}
+ {#each selectedSeason.episodes as episode}
+
playEpisodeFromCard(episode)}
+ >
+
+ {#if episode.still}
+
})
+ {:else}
+
+
+
+ {/if}
+
+
+
+
+
+
+ {formatEpisodeCode(episode)} · {episode.title || "Untitled"}
+
+
+ {#if episodeRuntime(episode)}
+
+
+ {episodeRuntime(episode)}
+
+ {/if}
+ {#if formatAirDate(episode.aired)}
+
+
+ {formatAirDate(episode.aired)}
+
+ {/if}
+ {#if formatVideoInfo(episode.mediaInfo?.video)}
+
+
+ {formatVideoInfo(episode.mediaInfo.video)}
+
+ {/if}
+ {#if formatAudioInfo(episode.mediaInfo?.audio)}
+
+
+ {formatAudioInfo(episode.mediaInfo.audio)}
+
+ {/if}
+
+
+ {episode.overview || "No overview available."}
+
+
+
+ {/each}
+ {:else}
+
No episodes found for this season.
+ {/if}
+
+
+
+{/if}
+
+{#if showPlayerModal && selectedVideo}
+
+
+
+
+
+ {#key encName}
+
+ {/key}
+
+
+
+
+
+
+
+
+
+
+
+ {formatTime(currentTime)} / {formatTime(duration)}
+
+
+
+
+
+
+
+{/if}
+
+
diff --git a/client/src/routes/Transfers.svelte b/client/src/routes/Transfers.svelte
index 04eba63..0da202a 100644
--- a/client/src/routes/Transfers.svelte
+++ b/client/src/routes/Transfers.svelte
@@ -282,7 +282,8 @@
try {
const params = new URLSearchParams({
query,
- type: "series"
+ type: "series",
+ scope: "anime"
});
if (mailruMatchYear.trim()) {
params.set("year", mailruMatchYear.trim());
diff --git a/client/src/stores/animeStore.js b/client/src/stores/animeStore.js
new file mode 100644
index 0000000..5acc79a
--- /dev/null
+++ b/client/src/stores/animeStore.js
@@ -0,0 +1,43 @@
+import { writable } from "svelte/store";
+import { apiFetch } from "../utils/api.js";
+
+export const animeCount = writable(0);
+let requestSeq = 0;
+let lastValue = 0;
+let zeroTimer = null;
+
+export async function refreshAnimeCount() {
+ const ticket = ++requestSeq;
+ try {
+ const resp = await apiFetch("/api/anime");
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ const list = await resp.json();
+ if (ticket !== requestSeq) return;
+ const nextVal = Array.isArray(list) ? list.length : 0;
+ if (nextVal > 0) {
+ if (zeroTimer) {
+ clearTimeout(zeroTimer);
+ zeroTimer = null;
+ }
+ lastValue = nextVal;
+ animeCount.set(nextVal);
+ } else if (lastValue > 0) {
+ if (zeroTimer) clearTimeout(zeroTimer);
+ const zeroTicket = requestSeq;
+ zeroTimer = setTimeout(() => {
+ if (zeroTicket === requestSeq) {
+ lastValue = 0;
+ animeCount.set(0);
+ }
+ zeroTimer = null;
+ }, 500);
+ } else {
+ lastValue = 0;
+ animeCount.set(0);
+ }
+ } catch (err) {
+ console.warn("⚠️ Anime sayacı güncellenemedi:", err?.message || err);
+ // Hata durumunda mevcut değeri koru, titreşimi önle
+ }
+}
+
diff --git a/server/server.js b/server/server.js
index d72213c..d1a33a2 100644
--- a/server/server.js
+++ b/server/server.js
@@ -38,6 +38,10 @@ if (!fs.existsSync(DOWNLOAD_DIR))
const TRASH_DIR = path.join(__dirname, "trash");
if (!fs.existsSync(TRASH_DIR))
fs.mkdirSync(TRASH_DIR, { recursive: true });
+const ROOT_TRASH_PREFIX = "__root__";
+const ROOT_TRASH_DIR = path.join(TRASH_DIR, "root");
+if (!fs.existsSync(ROOT_TRASH_DIR))
+ fs.mkdirSync(ROOT_TRASH_DIR, { recursive: true });
// --- Thumbnail cache klasörü ---
const CACHE_DIR = path.join(__dirname, "cache");
@@ -46,7 +50,10 @@ const VIDEO_THUMB_ROOT = path.join(THUMBNAIL_DIR, "videos");
const IMAGE_THUMB_ROOT = path.join(THUMBNAIL_DIR, "images");
const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
+const ANIME_DATA_ROOT = path.join(CACHE_DIR, "anime_data");
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
+const ANIME_ROOT_FOLDER = "_anime";
+const ROOT_TRASH_REGISTRY = path.join(CACHE_DIR, "root-trash.json");
const MUSIC_EXTENSIONS = new Set([
".mp3",
".m4a",
@@ -65,6 +72,7 @@ for (const dir of [
IMAGE_THUMB_ROOT,
MOVIE_DATA_ROOT,
TV_DATA_ROOT,
+ ANIME_DATA_ROOT,
YT_DATA_ROOT
]) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -1437,6 +1445,14 @@ function finalizeMailRuJob(job, exitCode) {
const relPath = job.savePath === DOWNLOAD_DIR
? String(job.fileName || "")
: path.join(job.folderId || "", job.fileName || "").replace(/\\/g, "/");
+ const seriesInfo = job.match?.seriesInfo || null;
+ if (seriesInfo) {
+ extractMediaInfo(filePath)
+ .then((mediaInfo) =>
+ ensureSeriesData(ANIME_ROOT_FOLDER, job.fileName, seriesInfo, mediaInfo)
+ )
+ .catch(() => null);
+ }
attachMailRuThumbnail(job, filePath, relPath);
broadcastFileUpdate(relPath || "downloads");
scheduleSnapshotBroadcast();
@@ -2199,6 +2215,109 @@ function normalizeTrashPath(value) {
return String(value).replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}
+function isRootTrashName(value) {
+ const normalized = normalizeTrashPath(value);
+ return (
+ normalized === ROOT_TRASH_PREFIX ||
+ normalized.startsWith(`${ROOT_TRASH_PREFIX}/`)
+ );
+}
+
+function parseRootTrashName(value) {
+ const normalized = normalizeTrashPath(value);
+ if (normalized === ROOT_TRASH_PREFIX) return "";
+ if (!normalized.startsWith(`${ROOT_TRASH_PREFIX}/`)) return null;
+ return normalized.slice(ROOT_TRASH_PREFIX.length + 1);
+}
+
+function readRootTrashRegistry() {
+ if (!fs.existsSync(ROOT_TRASH_REGISTRY)) return { items: [] };
+ try {
+ const raw = JSON.parse(fs.readFileSync(ROOT_TRASH_REGISTRY, "utf-8"));
+ const items = Array.isArray(raw?.items) ? raw.items : [];
+ return { items: items.filter(Boolean) };
+ } catch (err) {
+ console.warn(`⚠️ root-trash okunamadı (${ROOT_TRASH_REGISTRY}): ${err.message}`);
+ return { items: [] };
+ }
+}
+
+function writeRootTrashRegistry(registry) {
+ const items = Array.isArray(registry?.items) ? registry.items : [];
+ try {
+ fs.writeFileSync(
+ ROOT_TRASH_REGISTRY,
+ JSON.stringify({ updatedAt: Date.now(), items }, null, 2),
+ "utf-8"
+ );
+ } catch (err) {
+ console.warn(`⚠️ root-trash yazılamadı (${ROOT_TRASH_REGISTRY}): ${err.message}`);
+ }
+}
+
+function addRootTrashEntry(relPath, fullPath, stats) {
+ const safeRel = sanitizeRelative(relPath);
+ if (!safeRel || safeRel.includes("/")) return null;
+ if (!fullPath || !fs.existsSync(fullPath)) return null;
+ const registry = readRootTrashRegistry();
+ const baseName = path.basename(safeRel);
+ const storedName = `${Date.now()}_${baseName}`.replace(/[\\/:*?"<>|]+/g, "_");
+ const storedPath = path.join(ROOT_TRASH_DIR, storedName);
+ try {
+ fs.renameSync(fullPath, storedPath);
+ } catch (err) {
+ // Cross-device rename (EXDEV) durumunda kopyala+sil fallback'i uygula.
+ if (err?.code === "EXDEV") {
+ try {
+ if (stats?.isDirectory?.()) {
+ fs.cpSync(fullPath, storedPath, { recursive: true });
+ fs.rmSync(fullPath, { recursive: true, force: true });
+ } else {
+ fs.copyFileSync(fullPath, storedPath);
+ fs.rmSync(fullPath, { force: true });
+ }
+ } catch (copyErr) {
+ console.warn(
+ `⚠️ root-trash EXDEV fallback hatası (${fullPath}): ${copyErr.message}`
+ );
+ return null;
+ }
+ } else {
+ console.warn(`⚠️ root-trash taşıma hatası (${fullPath}): ${err.message}`);
+ return null;
+ }
+ }
+ const nextItems = registry.items.filter((item) => item.originalName !== baseName);
+ nextItems.push({
+ originalName: baseName,
+ storedName,
+ deletedAt: Date.now(),
+ type: stats?.isDirectory?.()
+ ? "inode/directory"
+ : mime.lookup(fullPath) || "application/octet-stream"
+ });
+ writeRootTrashRegistry({ items: nextItems });
+ return nextItems[nextItems.length - 1];
+}
+
+function removeRootTrashEntry(originalName) {
+ const safeName = sanitizeRelative(originalName);
+ if (!safeName || safeName.includes("/")) return null;
+ const registry = readRootTrashRegistry();
+ const kept = [];
+ let removed = null;
+ for (const item of registry.items) {
+ if (item.originalName === safeName && !removed) {
+ removed = item;
+ continue;
+ }
+ kept.push(item);
+ }
+ if (!removed) return null;
+ writeRootTrashRegistry({ items: kept });
+ return removed;
+}
+
function trashFlagPathFor(rootFolder) {
const safeRoot = sanitizeRelative(rootFolder);
if (!safeRoot) return null;
@@ -2439,10 +2558,14 @@ function resolveMovieDataAbsolute(relPath) {
function resolveTvDataAbsolute(relPath) {
const normalized = sanitizeRelative(relPath);
- const resolved = path.resolve(TV_DATA_ROOT, normalized);
+ const firstSegment = normalized.split("/").filter(Boolean)[0] || "";
+ const { rootFolder } = parseTvSeriesKey(firstSegment);
+ const dataRoot =
+ rootFolder === ANIME_ROOT_FOLDER ? ANIME_DATA_ROOT : TV_DATA_ROOT;
+ const resolved = path.resolve(dataRoot, normalized);
if (
- resolved !== TV_DATA_ROOT &&
- !resolved.startsWith(TV_DATA_ROOT + path.sep)
+ resolved !== dataRoot &&
+ !resolved.startsWith(dataRoot + path.sep)
) {
return null;
}
@@ -2720,6 +2843,34 @@ function parseSeriesInfo(rawName) {
};
}
+function parseAnimeSeriesInfo(rawName) {
+ if (!rawName) return null;
+ const parsed = parseSeriesInfo(rawName);
+ if (parsed) return parsed;
+ const withoutExt = String(rawName || "").replace(/\.[^/.]+$/, "");
+ const match = withoutExt.match(
+ /(.+?)[\s._-]*S(\d{1,2})xE(\d{1,2})/i
+ );
+ if (!match) return null;
+ const rawTitle = match[1]
+ .replace(/[._]+/g, " ")
+ .replace(/\s+-\s+/g, " - ")
+ .replace(/[-_]+/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+ if (!rawTitle) return null;
+ const season = Number(match[2]);
+ const episode = Number(match[3]);
+ if (!Number.isFinite(season) || !Number.isFinite(episode)) return null;
+ return {
+ title: titleCase(rawTitle),
+ searchTitle: rawTitle,
+ season,
+ episode,
+ key: `S${String(season).padStart(2, "0")}E${String(episode).padStart(2, "0")}`
+ };
+}
+
async function tmdbFetch(endpoint, params = {}) {
if (!TMDB_API_KEY) return null;
const url = new URL(`${TMDB_BASE_URL}${endpoint}`);
@@ -3312,9 +3463,39 @@ async function fetchTvdbEpisode(seriesId, season, episode) {
return seasonEpisodes.get(episode) || null;
}
-async function fetchTvdbEpisodeExtended(episodeId) {
+function tvdbPickTranslation(list, field, preferEn = false) {
+ if (!Array.isArray(list)) return null;
+ const preferred = preferEn
+ ? ["en", "eng", "english", "en-us"]
+ : ["tr", "tur", "turkish", "tr-tr", "tr_tur"];
+ const fallback = preferEn
+ ? ["tr", "tur", "turkish", "tr-tr", "tr_tur"]
+ : ["en", "eng", "english", "en-us"];
+ const pickByLang = (langs) =>
+ langs
+ .map((lng) => lng.toLowerCase())
+ .map((lng) =>
+ list.find((item) => {
+ const code = String(
+ item?.language ||
+ item?.iso6391 ||
+ item?.iso_639_1 ||
+ item?.locale ||
+ item?.languageCode ||
+ ""
+ ).toLowerCase();
+ return code === lng;
+ })
+ )
+ .find(Boolean);
+ const match = pickByLang(preferred) || pickByLang(fallback);
+ if (!match) return null;
+ return match[field] ?? match.value ?? match.translation?.[field] ?? null;
+}
+
+async function fetchTvdbEpisodeExtended(episodeId, preferEn = false) {
if (!episodeId) return null;
- const cacheKey = `episode-${episodeId}-extended`;
+ const cacheKey = `episode-${episodeId}-extended-${preferEn ? "en" : "tr"}`;
if (tvdbEpisodeDetailCache.has(cacheKey))
return tvdbEpisodeDetailCache.get(cacheKey);
const resp = await tvdbFetch(
@@ -3364,44 +3545,21 @@ async function fetchTvdbEpisodeExtended(episodeId) {
const nameTranslations =
translations.nameTranslations || translations.names || [];
- const pickTranslation = (list, field) => {
- if (!Array.isArray(list)) return null;
- const preferred = ["tr", "turkish", "tr-tr", "tr_tur"];
- const fallback = ["en", "english", "en-us", "eng"];
- const pickByLang = (langs) =>
- langs
- .map((lng) => lng.toLowerCase())
- .map((lng) =>
- list.find((item) => {
- const code = String(
- item?.language ||
- item?.iso6391 ||
- item?.iso_639_1 ||
- item?.locale ||
- item?.languageCode ||
- ""
- ).toLowerCase();
- return code === lng;
- })
- )
- .find(Boolean);
- const preferredMatch = pickByLang(preferred) || pickByLang(fallback);
- if (!preferredMatch) return null;
- return (
- preferredMatch[field] ??
- preferredMatch.value ??
- preferredMatch.translation?.[field] ??
- null
- );
- };
-
if (!base.overview) {
- const localizedOverview = pickTranslation(overviewTranslations, "overview");
+ const localizedOverview = tvdbPickTranslation(
+ overviewTranslations,
+ "overview",
+ preferEn
+ );
if (localizedOverview) base.overview = localizedOverview;
}
if (!base.name) {
- const localizedName = pickTranslation(nameTranslations, "name");
+ const localizedName = tvdbPickTranslation(
+ nameTranslations,
+ "name",
+ preferEn
+ );
if (localizedName) base.name = localizedName;
}
@@ -3504,7 +3662,7 @@ async function ensureSeriesData(
}
}
- if (!seriesData && candidateKeys.length) {
+ if (!seriesData && candidateKeys.length && normalizedRoot !== ANIME_ROOT_FOLDER) {
for (const key of candidateKeys) {
const candidatePaths = tvSeriesPathsByKey(key);
if (!fs.existsSync(candidatePaths.metadata)) continue;
@@ -3521,7 +3679,11 @@ async function ensureSeriesData(
}
const legacyPaths = tvSeriesPaths(normalizedRoot);
- if (!seriesData && fs.existsSync(legacyPaths.metadata)) {
+ if (
+ !seriesData &&
+ normalizedRoot !== ANIME_ROOT_FOLDER &&
+ fs.existsSync(legacyPaths.metadata)
+ ) {
try {
seriesData = JSON.parse(fs.readFileSync(legacyPaths.metadata, "utf-8")) || {};
existingPaths = legacyPaths;
@@ -3596,32 +3758,35 @@ async function ensureSeriesData(
translations.overviewTranslations ||
translations.overviews ||
[];
- const localizedName =
- nameTranslations.find((t) =>
- ["tr", "tur", "turkish"].includes(String(t?.language || t?.iso6391).toLowerCase())
- )?.value ||
- nameTranslations.find((t) =>
- ["en", "eng", "english"].includes(String(t?.language || t?.iso6391).toLowerCase())
- )?.value ||
- null;
- const localizedOverview =
- overviewTranslations.find((t) =>
- ["tr", "tur", "turkish"].includes(String(t?.language || t?.iso6391).toLowerCase())
- )?.overview ||
- overviewTranslations.find((t) =>
- ["en", "eng", "english"].includes(String(t?.language || t?.iso6391).toLowerCase())
- )?.overview ||
- null;
+ const preferEn = rootFolder === ANIME_ROOT_FOLDER;
+ const localizedName = tvdbPickTranslation(
+ nameTranslations,
+ "name",
+ preferEn
+ );
+ const localizedOverview = tvdbPickTranslation(
+ overviewTranslations,
+ "overview",
+ preferEn
+ );
- seriesData.name =
- seriesData.name ||
- info.name ||
- info.seriesName ||
- localizedName ||
- seriesInfo.title;
+ if (preferEn && localizedName) {
+ seriesData.name = localizedName;
+ } else {
+ seriesData.name =
+ seriesData.name ||
+ info.name ||
+ info.seriesName ||
+ localizedName ||
+ seriesInfo.title;
+ }
seriesData.slug = seriesData.slug || info.slug || info.slugged || null;
- seriesData.overview =
- seriesData.overview || info.overview || localizedOverview || "";
+ if (preferEn && localizedOverview) {
+ seriesData.overview = localizedOverview;
+ } else {
+ seriesData.overview =
+ seriesData.overview || info.overview || localizedOverview || "";
+ }
const firstAired =
info.firstAired ||
info.firstAirDate ||
@@ -3810,7 +3975,8 @@ async function ensureSeriesData(
!detailedEpisode.name)
) {
const extendedEpisode = await fetchTvdbEpisodeExtended(
- detailedEpisode.id
+ detailedEpisode.id,
+ preferEn
);
if (extendedEpisode) {
detailedEpisode = {
@@ -3997,9 +4163,20 @@ function parseTvSeriesKey(key) {
return { rootFolder, seriesId: suffix || null, key: normalized };
}
+function tvDataRootForRoot(rootFolder) {
+ const safeRoot = sanitizeRelative(rootFolder);
+ return safeRoot === ANIME_ROOT_FOLDER ? ANIME_DATA_ROOT : TV_DATA_ROOT;
+}
+
+function tvDataRootForKey(key) {
+ const { rootFolder } = parseTvSeriesKey(key);
+ return tvDataRootForRoot(rootFolder);
+}
+
function tvSeriesPathsByKey(key) {
const normalizedKey = sanitizeRelative(key);
- const dir = path.join(TV_DATA_ROOT, normalizedKey);
+ const dataRoot = tvDataRootForKey(normalizedKey);
+ const dir = path.join(dataRoot, normalizedKey);
return {
key: normalizedKey,
dir,
@@ -4008,7 +4185,8 @@ function tvSeriesPathsByKey(key) {
backdrop: path.join(dir, "backdrop.jpg"),
episodesDir: path.join(dir, "episodes"),
seasonsDir: path.join(dir, "seasons"),
- rootFolder: parseTvSeriesKey(normalizedKey).rootFolder
+ rootFolder: parseTvSeriesKey(normalizedKey).rootFolder,
+ dataRoot
};
}
@@ -4054,10 +4232,11 @@ function seasonAssetPaths(paths, seasonNumber) {
function listTvSeriesKeysForRoot(rootFolder) {
const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null;
if (!normalizedRoot) return [];
- if (!fs.existsSync(TV_DATA_ROOT)) return [];
+ const dataRoot = tvDataRootForRoot(normalizedRoot);
+ if (!fs.existsSync(dataRoot)) return [];
const keys = [];
try {
- const entries = fs.readdirSync(TV_DATA_ROOT, { withFileTypes: true });
+ const entries = fs.readdirSync(dataRoot, { withFileTypes: true });
for (const dirent of entries) {
if (!dirent.isDirectory()) continue;
const name = dirent.name;
@@ -4070,7 +4249,7 @@ function listTvSeriesKeysForRoot(rootFolder) {
}
} catch (err) {
console.warn(
- `⚠️ TV metadata dizini listelenemedi (${TV_DATA_ROOT}): ${err.message}`
+ `⚠️ TV metadata dizini listelenemedi (${dataRoot}): ${err.message}`
);
}
return keys;
@@ -4644,7 +4823,8 @@ function renameRootCaches(oldRoot, newRoot) {
VIDEO_THUMB_ROOT,
IMAGE_THUMB_ROOT,
MOVIE_DATA_ROOT,
- TV_DATA_ROOT
+ TV_DATA_ROOT,
+ ANIME_DATA_ROOT
];
for (const base of pairs) {
@@ -5763,8 +5943,21 @@ app.delete("/api/file", requireAuth, (req, res) => {
const safePath = sanitizeRelative(filePath);
const fullPath = path.join(DOWNLOAD_DIR, safePath);
- const folderId = (safePath.split(/[\/]/)[0] || "").trim();
- const rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
+ let folderId = (safePath.split(/[\/]/)[0] || "").trim();
+ let rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
+ let folderIsDirectory = false;
+ if (rootDir && fs.existsSync(rootDir)) {
+ try {
+ folderIsDirectory = fs.statSync(rootDir).isDirectory();
+ } catch (err) {
+ folderIsDirectory = false;
+ }
+ }
+ // Kök dosyalarda ilk segment dosya adıdır; klasör değilse root davranışı uygula
+ if (folderId && !folderIsDirectory) {
+ folderId = "";
+ rootDir = null;
+ }
let mediaFlags = { movies: false, tv: false };
let stats = null;
@@ -5776,7 +5969,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
}
if (!stats || !fs.existsSync(fullPath)) {
- if (folderId && (!rootDir || !fs.existsSync(rootDir))) {
+ if (folderId && folderIsDirectory && (!rootDir || !fs.existsSync(rootDir))) {
purgeRootFolder(folderId);
broadcastFileUpdate(folderId);
return res.json({ ok: true, alreadyRemoved: true });
@@ -5788,7 +5981,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
const isDirectory = stats.isDirectory();
const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/");
let trashEntry = null;
- if (folderId && rootDir && fs.existsSync(rootDir) && fs.statSync(rootDir).isDirectory()) {
+ if (folderId && folderIsDirectory && rootDir) {
const infoBeforeDelete = readInfoForRoot(folderId);
mediaFlags = detectMediaFlagsForPath(
infoBeforeDelete,
@@ -5799,7 +5992,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
mediaFlags = { movies: false, tv: false };
}
- if (folderId && rootDir && fs.existsSync(rootDir) && fs.statSync(rootDir).isDirectory()) {
+ if (folderId && folderIsDirectory && rootDir) {
trashEntry = addTrashEntry(folderId, {
path: relWithinRoot,
originalPath: safePath,
@@ -5827,11 +6020,17 @@ app.delete("/api/file", requireAuth, (req, res) => {
}
if (!folderId) {
- // Kök klasöre ait olmayan dosyaları doğrudan sil
- if (fs.existsSync(fullPath)) {
- fs.rmSync(fullPath, { recursive: true, force: true });
+ // Kök dosyaları root-trash sistemine taşı
+ const rootTrashEntry = addRootTrashEntry(safePath, fullPath, stats);
+ if (!rootTrashEntry) {
+ return res.status(500).json({ error: "Kök dosya çöpe taşınamadı" });
}
+ // Anime cache/metadata'dan ilgili bölümü kaldır
+ removeSeriesEpisode(ANIME_ROOT_FOLDER, safePath);
removeThumbnailsForPath(safePath);
+ broadcastFileUpdate("downloads");
+ broadcastDiskSpace();
+ return res.json({ ok: true, filesRemoved: true, rootTrashed: true });
}
if (folderId) {
@@ -6307,6 +6506,42 @@ app.get("/api/trash", requireAuth, (req, res) => {
}
}
+ // Root trash öğelerini ekle (kök dosyalar için)
+ const rootRegistry = readRootTrashRegistry();
+ for (const item of rootRegistry.items) {
+ const originalName = sanitizeRelative(item.originalName || "");
+ const storedName = sanitizeRelative(item.storedName || "");
+ if (!originalName || !storedName) continue;
+ const storedPath = path.join(ROOT_TRASH_DIR, storedName);
+ if (!fs.existsSync(storedPath)) {
+ removeRootTrashEntry(originalName);
+ continue;
+ }
+ let stat = null;
+ try {
+ stat = fs.statSync(storedPath);
+ } catch (err) {
+ continue;
+ }
+ const isDirectory = stat?.isDirectory?.() || false;
+ const type = isDirectory
+ ? "inode/directory"
+ : mime.lookup(originalName) || item.type || "application/octet-stream";
+ const trashName = `${ROOT_TRASH_PREFIX}/${originalName}`;
+ result.push({
+ name: trashName,
+ trashName,
+ size: stat?.size ?? 0,
+ type,
+ isDirectory,
+ thumbnail: null,
+ mediaInfo: null,
+ movedAt: Number(item.deletedAt) || Date.now(),
+ originalPath: originalName,
+ folderId: ROOT_TRASH_PREFIX
+ });
+ }
+
result.sort((a, b) => (b.movedAt || 0) - (a.movedAt || 0));
res.json(result);
} catch (err) {
@@ -6316,15 +6551,91 @@ app.get("/api/trash", requireAuth, (req, res) => {
});
// --- 🗑️ Çöpten geri yükleme API (.trash flag sistemi) ---
-app.post("/api/trash/restore", requireAuth, (req, res) => {
+app.post("/api/trash/restore", requireAuth, async (req, res) => {
try {
const { trashName } = req.body;
-
+
if (!trashName) {
return res.status(400).json({ error: "trashName gerekli" });
}
-
+
const safeName = sanitizeRelative(trashName);
+ if (isRootTrashName(safeName)) {
+ const originalName = parseRootTrashName(safeName);
+ if (!originalName) {
+ return res.status(400).json({ error: "Geçersiz root trashName" });
+ }
+
+ const removed = removeRootTrashEntry(originalName);
+ if (!removed?.storedName) {
+ return res.status(404).json({ error: "Root çöp öğesi bulunamadı" });
+ }
+
+ const storedName = sanitizeRelative(removed.storedName);
+ const storedPath = path.join(ROOT_TRASH_DIR, storedName);
+ const targetPath = path.join(DOWNLOAD_DIR, originalName);
+
+ if (!fs.existsSync(storedPath)) {
+ return res
+ .status(404)
+ .json({ error: "Root çöp dosyası bulunamadı" });
+ }
+
+ ensureDirForFile(targetPath);
+ try {
+ fs.renameSync(storedPath, targetPath);
+ } catch (err) {
+ if (err?.code === "EXDEV") {
+ let stat = null;
+ try {
+ stat = fs.statSync(storedPath);
+ } catch (statErr) {
+ return res.status(500).json({ error: "Root çöp dosyası okunamadı" });
+ }
+ try {
+ if (stat?.isDirectory?.()) {
+ fs.cpSync(storedPath, targetPath, { recursive: true });
+ fs.rmSync(storedPath, { recursive: true, force: true });
+ } else {
+ fs.copyFileSync(storedPath, targetPath);
+ fs.rmSync(storedPath, { force: true });
+ }
+ } catch (copyErr) {
+ console.warn(
+ `⚠️ root-trash restore EXDEV hatası (${storedPath}): ${copyErr.message}`
+ );
+ return res
+ .status(500)
+ .json({ error: "Root çöp dosyası taşınamadı" });
+ }
+ } else {
+ throw err;
+ }
+ }
+
+ console.log(`♻️ Root öğe geri yüklendi: ${originalName}`);
+
+ const animeSeriesInfo = parseAnimeSeriesInfo(originalName);
+ if (animeSeriesInfo) {
+ const mediaInfo = await extractMediaInfo(targetPath).catch(() => null);
+ await ensureSeriesData(
+ ANIME_ROOT_FOLDER,
+ originalName,
+ animeSeriesInfo,
+ mediaInfo
+ );
+ }
+
+ broadcastFileUpdate("downloads");
+ broadcastDiskSpace();
+
+ return res.json({
+ success: true,
+ message: "Öğe başarıyla geri yüklendi",
+ folderId: "downloads"
+ });
+ }
+
const segments = safeName.split(/[\\/]/).filter(Boolean);
if (!segments.length) {
return res.status(400).json({ error: "Geçersiz trashName" });
@@ -6372,6 +6683,40 @@ app.delete("/api/trash", requireAuth, (req, res) => {
}
const safeName = sanitizeRelative(trashName);
+ if (isRootTrashName(safeName)) {
+ const originalName = parseRootTrashName(safeName);
+ if (!originalName) {
+ return res.status(400).json({ error: "Geçersiz root trashName" });
+ }
+
+ const removed = removeRootTrashEntry(originalName);
+ if (!removed?.storedName) {
+ return res.status(404).json({ error: "Root çöp öğesi bulunamadı" });
+ }
+
+ const storedName = sanitizeRelative(removed.storedName);
+ const storedPath = path.join(ROOT_TRASH_DIR, storedName);
+ if (fs.existsSync(storedPath)) {
+ try {
+ fs.rmSync(storedPath, { recursive: true, force: true });
+ } catch (err) {
+ console.warn(
+ `⚠️ Root çöp öğesi silinemedi (${storedPath}): ${err.message}`
+ );
+ }
+ }
+
+ console.log(`🗑️ Root öğe kalıcı olarak silindi: ${originalName}`);
+
+ broadcastFileUpdate("downloads");
+ broadcastDiskSpace();
+
+ return res.json({
+ success: true,
+ message: "Öğe tamamen silindi"
+ });
+ }
+
const segments = safeName.split(/[\\/]/).filter(Boolean);
if (!segments.length) {
return res.status(400).json({ error: "Geçersiz trashName" });
@@ -6803,15 +7148,28 @@ app.post("/api/mailru/match", requireAuth, async (req, res) => {
const safeSeason = Number(season) || 1;
const safeEpisode = Number(episode) || 1;
const title = metadata.title || metadata.name || "Anime";
+ const seriesInfo = {
+ title,
+ searchTitle: title,
+ season: safeSeason,
+ episode: safeEpisode,
+ key: `S${String(safeSeason).padStart(2, "0")}E${String(safeEpisode).padStart(2, "0")}`
+ };
job.match = {
id: metadata.id || null,
title,
season: safeSeason,
episode: safeEpisode,
- matchedAt: Date.now()
+ matchedAt: Date.now(),
+ rootFolder: ANIME_ROOT_FOLDER,
+ seriesInfo
};
job.fileName = formatMailRuSeriesFilename(title, safeSeason, safeEpisode);
job.title = job.fileName;
+ // Anime metadata'yı TVDB mantığıyla önceden hazırla (dosya henüz inmemiş olabilir)
+ ensureSeriesData(ANIME_ROOT_FOLDER, job.fileName, seriesInfo, null).catch(
+ () => null
+ );
const started = await beginMailRuDownload(job);
if (!started) {
return res.status(500).json({ ok: false, error: job.error || "Mail.ru indirimi başlatılamadı." });
@@ -6986,6 +7344,7 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
if (!paths || !fs.existsSync(paths.metadata)) continue;
const { rootFolder } = parseTvSeriesKey(key);
if (!rootFolder) continue;
+ if (rootFolder === ANIME_ROOT_FOLDER) continue;
const infoForFolder = readInfoForRoot(rootFolder) || {};
const infoFiles = infoForFolder.files || {};
@@ -7326,6 +7685,369 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
}
});
+function buildAnimeShows() {
+ if (!fs.existsSync(ANIME_DATA_ROOT)) {
+ return [];
+ }
+
+ const dirEntries = fs
+ .readdirSync(ANIME_DATA_ROOT, { withFileTypes: true })
+ .filter((d) => d.isDirectory());
+
+ const aggregated = new Map();
+
+ const mergeEpisode = (existing, incoming) => {
+ if (!existing) return incoming;
+ const merged = { ...existing, ...incoming };
+ if (existing.still && !incoming.still) merged.still = existing.still;
+ if (!existing.still && incoming.still) merged.still = incoming.still;
+ if (existing.mediaInfo && !incoming.mediaInfo) merged.mediaInfo = existing.mediaInfo;
+ if (!existing.mediaInfo && incoming.mediaInfo) merged.mediaInfo = incoming.mediaInfo;
+ if (existing.overview && !incoming.overview) merged.overview = existing.overview;
+ return merged;
+ };
+
+ for (const dirent of dirEntries) {
+ const key = sanitizeRelative(dirent.name);
+ if (!key) continue;
+ const paths = tvSeriesPathsByKey(key);
+ if (!paths || !fs.existsSync(paths.metadata)) continue;
+ const { rootFolder } = parseTvSeriesKey(key);
+ if (rootFolder !== ANIME_ROOT_FOLDER) continue;
+
+ let seriesData;
+ try {
+ seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8"));
+ } catch (err) {
+ console.warn(`⚠️ anime series.json okunamadı (${paths.metadata}): ${err.message}`);
+ continue;
+ }
+
+ const seasonsObj = seriesData?.seasons || {};
+ if (!Object.keys(seasonsObj).length) continue;
+
+ let dataChanged = false;
+ const showId = seriesData.id ?? seriesData.tvdbId ?? seriesData.slug ?? seriesData.name ?? key;
+ const showKey = String(showId).toLowerCase();
+ const record =
+ aggregated.get(showKey) ||
+ (() => {
+ const base = {
+ id: seriesData.id ?? seriesData.tvdbId ?? key,
+ title: seriesData.name || "Anime",
+ overview: seriesData.overview || "",
+ year: seriesData.year || null,
+ status: seriesData.status || null,
+ poster: fs.existsSync(paths.poster)
+ ? encodeTvDataPath(paths.key, "poster.jpg")
+ : null,
+ backdrop: fs.existsSync(paths.backdrop)
+ ? encodeTvDataPath(paths.key, "backdrop.jpg")
+ : null,
+ genres: new Set(
+ Array.isArray(seriesData.genres)
+ ? seriesData.genres
+ .map((g) => (typeof g === "string" ? g : g?.name || null))
+ .filter(Boolean)
+ : []
+ ),
+ seasons: new Map(),
+ primaryFolder: ANIME_ROOT_FOLDER,
+ folders: new Set([ANIME_ROOT_FOLDER])
+ };
+ aggregated.set(showKey, base);
+ return base;
+ })();
+
+ if (
+ seriesData.overview &&
+ seriesData.overview.length > (record.overview?.length || 0)
+ ) {
+ record.overview = seriesData.overview;
+ }
+ if (!record.status && seriesData.status) record.status = seriesData.status;
+ if (!record.year || (seriesData.year && Number(seriesData.year) < Number(record.year))) {
+ record.year = seriesData.year || record.year;
+ }
+ if (!record.poster && fs.existsSync(paths.poster)) {
+ record.poster = encodeTvDataPath(paths.key, "poster.jpg");
+ }
+ if (!record.backdrop && fs.existsSync(paths.backdrop)) {
+ record.backdrop = encodeTvDataPath(paths.key, "backdrop.jpg");
+ }
+ if (Array.isArray(seriesData.genres)) {
+ seriesData.genres
+ .map((g) => (typeof g === "string" ? g : g?.name || null))
+ .filter(Boolean)
+ .forEach((genre) => record.genres.add(genre));
+ }
+
+ for (const [seasonKey, rawSeason] of Object.entries(seasonsObj)) {
+ if (!rawSeason?.episodes) continue;
+ const seasonNumber = toFiniteNumber(
+ rawSeason.seasonNumber ?? rawSeason.number ?? seasonKey
+ );
+ if (!Number.isFinite(seasonNumber)) continue;
+
+ const seasonPaths = seasonAssetPaths(paths, seasonNumber);
+ const rawEpisodes = rawSeason.episodes || {};
+
+ for (const [episodeKey, rawEpisode] of Object.entries(rawEpisodes)) {
+ if (!rawEpisode || typeof rawEpisode !== "object") continue;
+ const relativeFile = (rawEpisode.file || "").replace(/\\/g, "/");
+ if (!relativeFile) continue;
+ const absEpisodePath = path.join(DOWNLOAD_DIR, relativeFile);
+ const controlPath = `${absEpisodePath}.aria2`;
+ const isComplete = fs.existsSync(absEpisodePath) && !fs.existsSync(controlPath);
+ if (!isComplete) {
+ delete rawEpisodes[episodeKey];
+ dataChanged = true;
+ }
+ }
+
+ if (!Object.keys(rawSeason.episodes || {}).length) {
+ delete seasonsObj[seasonKey];
+ dataChanged = true;
+ continue;
+ }
+
+ let seasonRecord = record.seasons.get(seasonNumber);
+ if (!seasonRecord) {
+ seasonRecord = {
+ seasonNumber,
+ name: rawSeason.name || `Season ${seasonNumber}`,
+ overview: rawSeason.overview || "",
+ poster: rawSeason.poster || null,
+ tvdbId: rawSeason.tvdbId || null,
+ slug: rawSeason.slug || null,
+ episodeCount: rawSeason.episodeCount || null,
+ episodes: new Map()
+ };
+ record.seasons.set(seasonNumber, seasonRecord);
+ }
+
+ if (!seasonRecord.poster && fs.existsSync(seasonPaths.poster)) {
+ const relPoster = path.relative(paths.dir, seasonPaths.poster);
+ seasonRecord.poster = encodeTvDataPath(paths.key, relPoster);
+ }
+
+ for (const [episodeKey, rawEpisode] of Object.entries(rawSeason.episodes)) {
+ if (!rawEpisode || typeof rawEpisode !== "object") continue;
+ const episodeNumber = toFiniteNumber(
+ rawEpisode.episodeNumber ?? rawEpisode.number ?? episodeKey
+ );
+ if (!Number.isFinite(episodeNumber)) continue;
+
+ const normalizedEpisode = { ...rawEpisode };
+ normalizedEpisode.seasonNumber = seasonNumber;
+ normalizedEpisode.episodeNumber = episodeNumber;
+ if (!normalizedEpisode.code) {
+ normalizedEpisode.code = `S${String(seasonNumber).padStart(2, "0")}E${String(
+ episodeNumber
+ ).padStart(2, "0")}`;
+ }
+
+ const relativeFile = (normalizedEpisode.file || "").replace(/\\/g, "/");
+ if (relativeFile) {
+ const absVideo = path.join(DOWNLOAD_DIR, relativeFile);
+ const ext = path.extname(relativeFile).toLowerCase();
+ if (fs.existsSync(absVideo) && VIDEO_EXTS.includes(ext)) {
+ normalizedEpisode.videoPath = relativeFile;
+ const stats = fs.statSync(absVideo);
+ normalizedEpisode.fileSize = Number(stats.size);
+ } else {
+ normalizedEpisode.videoPath = null;
+ }
+ }
+
+ normalizedEpisode.folder = ANIME_ROOT_FOLDER;
+
+ const existingEpisode = seasonRecord.episodes.get(episodeNumber);
+ seasonRecord.episodes.set(
+ episodeNumber,
+ mergeEpisode(existingEpisode, normalizedEpisode)
+ );
+ }
+
+ if (!seasonRecord.episodeCount && seasonRecord.episodes.size) {
+ seasonRecord.episodeCount = seasonRecord.episodes.size;
+ }
+ }
+
+ if (dataChanged) {
+ try {
+ seriesData.seasons = seasonsObj;
+ seriesData.updatedAt = Date.now();
+ fs.writeFileSync(paths.metadata, JSON.stringify(seriesData, null, 2), "utf-8");
+ } catch (err) {
+ console.warn(`⚠️ anime series.json güncellenemedi (${paths.metadata}): ${err.message}`);
+ }
+ }
+ }
+
+ const shows = Array.from(aggregated.values())
+ .map((record) => {
+ const seasons = Array.from(record.seasons.values())
+ .map((season) => {
+ const episodes = Array.from(season.episodes.values())
+ .filter((episode) => {
+ if (!episode?.videoPath) return false;
+ const ext = path.extname(episode.videoPath).toLowerCase();
+ if (!VIDEO_EXTS.includes(ext)) return false;
+ const absVideo = path.join(DOWNLOAD_DIR, episode.videoPath);
+ if (!fs.existsSync(absVideo)) return false;
+ const controlPath = `${absVideo}.aria2`;
+ return !fs.existsSync(controlPath);
+ })
+ .sort((a, b) => a.episodeNumber - b.episodeNumber);
+ return {
+ seasonNumber: season.seasonNumber,
+ name: season.name || `Season ${season.seasonNumber}`,
+ overview: season.overview || "",
+ poster: season.poster || null,
+ tvdbSeasonId: season.tvdbId || null,
+ slug: season.slug || null,
+ episodeCount: season.episodeCount || episodes.length,
+ episodes
+ };
+ })
+ .sort((a, b) => a.seasonNumber - b.seasonNumber);
+
+ return {
+ folder: record.primaryFolder,
+ id: record.id || record.title,
+ title: record.title,
+ overview: record.overview || "",
+ year: record.year || null,
+ genres: Array.from(record.genres).filter(Boolean),
+ status: record.status || null,
+ poster: record.poster || null,
+ backdrop: record.backdrop || null,
+ seasons,
+ folders: Array.from(record.folders)
+ };
+ })
+ .filter((show) => show.seasons.length > 0);
+
+ shows.sort((a, b) => a.title.localeCompare(b.title, "en"));
+ return shows;
+}
+
+async function rebuildAnimeMetadata({ clearCache = false } = {}) {
+ if (clearCache && fs.existsSync(ANIME_DATA_ROOT)) {
+ try {
+ fs.rmSync(ANIME_DATA_ROOT, { recursive: true, force: true });
+ console.log("🧹 Anime cache temizlendi.");
+ } catch (err) {
+ console.warn(
+ `⚠️ Anime cache temizlenemedi (${ANIME_DATA_ROOT}): ${err.message}`
+ );
+ }
+ }
+
+ if (!fs.existsSync(ANIME_DATA_ROOT)) {
+ fs.mkdirSync(ANIME_DATA_ROOT, { recursive: true });
+ }
+ if (clearCache) {
+ tvdbSeriesCache.clear();
+ tvdbEpisodeCache.clear();
+ tvdbEpisodeDetailCache.clear();
+ }
+
+ let processed = 0;
+ let rootEntries = [];
+ try {
+ rootEntries = fs.readdirSync(DOWNLOAD_DIR, { withFileTypes: true });
+ } catch (err) {
+ console.warn(`⚠️ downloads kökü okunamadı: ${err.message}`);
+ }
+
+ for (const dirent of rootEntries) {
+ if (!dirent.isFile()) continue;
+ const safeName = sanitizeRelative(dirent.name);
+ if (!safeName || safeName.startsWith(".")) continue;
+ if (safeName.endsWith(".aria2")) continue;
+ const absPath = path.join(DOWNLOAD_DIR, safeName);
+ if (!fs.existsSync(absPath)) continue;
+ const mimeType = mime.lookup(absPath) || "";
+ if (!String(mimeType).startsWith("video/")) continue;
+ const seriesInfo = parseAnimeSeriesInfo(safeName);
+ if (!seriesInfo) continue;
+ const mediaInfo = await extractMediaInfo(absPath).catch(() => null);
+ await ensureSeriesData(
+ ANIME_ROOT_FOLDER,
+ safeName,
+ seriesInfo,
+ mediaInfo
+ );
+ processed += 1;
+ }
+
+ const shows = buildAnimeShows();
+ for (const show of shows) {
+ for (const season of show.seasons || []) {
+ for (const episode of season.episodes || []) {
+ if (!episode?.videoPath) continue;
+ const absVideo = path.join(DOWNLOAD_DIR, episode.videoPath);
+ if (!fs.existsSync(absVideo)) continue;
+ const seriesInfo = {
+ title: show.title,
+ searchTitle: show.title,
+ season: season.seasonNumber,
+ episode: episode.episodeNumber,
+ key: episode.code || `S${String(season.seasonNumber).padStart(2, "0")}E${String(
+ episode.episodeNumber
+ ).padStart(2, "0")}`
+ };
+ const mediaInfo = await extractMediaInfo(absVideo).catch(() => null);
+ await ensureSeriesData(ANIME_ROOT_FOLDER, episode.videoPath, seriesInfo, mediaInfo);
+ processed += 1;
+ }
+ }
+ }
+ return processed;
+}
+
+app.get("/api/anime", requireAuth, (req, res) => {
+ try {
+ const shows = buildAnimeShows();
+ res.json(shows);
+ } catch (err) {
+ console.error("🧿 Anime API error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.post("/api/anime/refresh", requireAuth, async (req, res) => {
+ if (!TVDB_API_KEY) {
+ return res
+ .status(400)
+ .json({ error: "TVDB API erişimi için gerekli anahtar tanımlı değil." });
+ }
+ try {
+ const processed = await rebuildAnimeMetadata();
+ res.json({ ok: true, processed });
+ } catch (err) {
+ console.error("🧿 Anime refresh error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.post("/api/anime/rescan", requireAuth, async (req, res) => {
+ if (!TVDB_API_KEY) {
+ return res
+ .status(400)
+ .json({ error: "TVDB API erişimi için gerekli anahtar tanımlı değil." });
+ }
+ try {
+ const processed = await rebuildAnimeMetadata({ clearCache: true });
+ res.json({ ok: true, processed });
+ } catch (err) {
+ console.error("🧿 Anime rescan error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
function collectMusicEntries() {
const entries = [];
const dirEntries = fs
@@ -7805,12 +8527,13 @@ app.get("/api/disk-space", requireAuth, async (req, res) => {
// --- 🔍 TMDB/TVDB Arama Endpoint'i ---
app.get("/api/search/metadata", requireAuth, async (req, res) => {
try {
- const { query, year, type } = req.query;
-
+ const { query, year, type, scope } = req.query;
+ const preferEnForSeries = type === "series" && scope === "anime";
+
if (!query) {
return res.status(400).json({ error: "query parametresi gerekli" });
}
-
+
if (type === "movie") {
// TMDB Film Araması
if (!TMDB_API_KEY) {
@@ -7882,22 +8605,22 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
if (!TVDB_API_KEY) {
return res.status(400).json({ error: "TVDB API key tanımlı değil" });
}
-
+
const params = new URLSearchParams({ type: "series", query: query });
const resp = await tvdbFetch(`/search?${params.toString()}`);
-
+
if (!resp || !resp.data) {
return res.json({ results: [] });
}
-
+
const allData = Array.isArray(resp.data) ? resp.data : [];
-
+
const resultsWithDetails = await Promise.all(
allData.slice(0, 20).map(async (item) => {
try {
const seriesId = item.tvdb_id || item.id;
const extended = await fetchTvdbSeriesExtended(seriesId);
-
+
if (extended) {
const info = extended.series || extended;
const artworks = Array.isArray(extended.artworks) ? extended.artworks : [];
@@ -7905,11 +8628,28 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
const type = String(a?.type || a?.artworkType || "").toLowerCase();
return type.includes("poster") || type === "series" || type === "2";
});
-
+
+ const translations =
+ extended.translations || info.translations || {};
+ const nameTranslations =
+ translations.nameTranslations || translations.names || [];
+ const overviewTranslations =
+ translations.overviewTranslations || translations.overviews || [];
+ const localizedName = tvdbPickTranslation(
+ nameTranslations,
+ "name",
+ preferEnForSeries
+ );
+ const localizedOverview = tvdbPickTranslation(
+ overviewTranslations,
+ "overview",
+ preferEnForSeries
+ );
+
const genres = Array.isArray(info.genres)
? info.genres.map(g => typeof g === "string" ? g : g?.name || g?.genre).filter(Boolean)
: [];
-
+
// Yıl bilgisini çeşitli yerlerden al
let seriesYear = null;
if (info.year) {
@@ -7921,12 +8661,12 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
const yearMatch = dateStr.match(/(\d{4})/);
if (yearMatch) seriesYear = Number(yearMatch[1]);
}
-
+
return {
id: seriesId,
- title: info.name || item.name,
+ title: localizedName || info.name || item.name,
year: seriesYear,
- overview: info.overview || item.overview || "",
+ overview: localizedOverview || info.overview || item.overview || "",
poster: posterArtwork?.image ? tvdbImageUrl(posterArtwork.image) : (item.image ? tvdbImageUrl(item.image) : null),
genres: genres,
status: info.status?.name || info.status || null,
@@ -7936,7 +8676,7 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
} catch (err) {
console.warn(`⚠️ Dizi detayı alınamadı:`, err.message);
}
-
+
// Fallback için yıl bilgisini al
let itemYear = null;
if (item.year) {
@@ -7946,7 +8686,7 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
const yearMatch = dateStr.match(/(\d{4})/);
if (yearMatch) itemYear = Number(yearMatch[1]);
}
-
+
return {
id: item.tvdb_id || item.id,
title: item.name || item.seriesName,
@@ -7957,23 +8697,23 @@ app.get("/api/search/metadata", requireAuth, async (req, res) => {
};
})
);
-
+
// Yıl filtresi detaylı bilgiler alındıktan SONRA uygula
let filtered = resultsWithDetails.filter(Boolean);
if (year && year.trim()) {
const targetYear = Number(year);
console.log(`🔍 TVDB Yıl filtresi uygulanıyor: ${targetYear}`);
-
+
filtered = filtered.filter(item => {
const itemYear = item.year ? Number(item.year) : null;
const matches = itemYear && itemYear === targetYear;
console.log(` - ${item.title}: yıl=${itemYear}, eşleşme=${matches}`);
return matches;
});
-
+
console.log(`🔍 Yıl filtresinden sonra: ${filtered.length} sonuç`);
}
-
+
res.json({ results: filtered.slice(0, 10) });
} else {
res.status(400).json({ error: "type parametresi 'movie' veya 'series' olmalı" });