+
+
+ {#if loading}
+ Yükleniyor…
+ {:else if error}
+ {error}
+ {:else if !items.length}
+ Henüz müzik videosu yok.
+ {:else}
+
+ {#each items as item, idx (item.id)}
+
+
{String(idx + 1).padStart(2, "0")}
+
+ {#if thumbnailURL(item)}
+
})
+ {:else}
+
+
+
+ {/if}
+
+
+
{cleanFileName(item.title)}
+
{sourceLabel(item)}
+
+
+ {formatDuration(item.mediaInfo?.format?.duration || item.duration)}
+
+
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/client/src/stores/movieStore.js b/client/src/stores/movieStore.js
index 8bd17ae..2060dd2 100644
--- a/client/src/stores/movieStore.js
+++ b/client/src/stores/movieStore.js
@@ -2,15 +2,41 @@ import { writable } from "svelte/store";
import { apiFetch } from "../utils/api.js";
export const movieCount = writable(0);
+let requestSeq = 0;
+let lastValue = 0;
+let zeroTimer = null;
export async function refreshMovieCount() {
+ const ticket = ++requestSeq;
try {
const resp = await apiFetch("/api/movies");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const list = await resp.json();
- movieCount.set(Array.isArray(list) ? list.length : 0);
+ if (ticket !== requestSeq) return;
+ const nextVal = Array.isArray(list) ? list.length : 0;
+ if (nextVal > 0) {
+ if (zeroTimer) {
+ clearTimeout(zeroTimer);
+ zeroTimer = null;
+ }
+ lastValue = nextVal;
+ movieCount.set(nextVal);
+ } else if (lastValue > 0) {
+ if (zeroTimer) clearTimeout(zeroTimer);
+ const zeroTicket = requestSeq;
+ zeroTimer = setTimeout(() => {
+ if (zeroTicket === requestSeq) {
+ lastValue = 0;
+ movieCount.set(0);
+ }
+ zeroTimer = null;
+ }, 500);
+ } else {
+ lastValue = 0;
+ movieCount.set(0);
+ }
} catch (err) {
console.warn("⚠️ Movie count güncellenemedi:", err?.message || err);
- movieCount.set(0);
+ // Hata durumunda mevcut değeri koru, titreşimi önle
}
}
diff --git a/client/src/stores/musicStore.js b/client/src/stores/musicStore.js
new file mode 100644
index 0000000..abe71f2
--- /dev/null
+++ b/client/src/stores/musicStore.js
@@ -0,0 +1,42 @@
+import { writable } from "svelte/store";
+import { apiFetch } from "../utils/api.js";
+
+export const musicCount = writable(0);
+let requestSeq = 0;
+let lastValue = 0;
+let zeroTimer = null;
+
+export async function refreshMusicCount() {
+ const ticket = ++requestSeq;
+ try {
+ const resp = await apiFetch("/api/music");
+ 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;
+ musicCount.set(nextVal);
+ } else if (lastValue > 0) {
+ if (zeroTimer) clearTimeout(zeroTimer);
+ const zeroTicket = requestSeq;
+ zeroTimer = setTimeout(() => {
+ if (zeroTicket === requestSeq) {
+ lastValue = 0;
+ musicCount.set(0);
+ }
+ zeroTimer = null;
+ }, 500);
+ } else {
+ lastValue = 0;
+ musicCount.set(0);
+ }
+ } catch (err) {
+ console.warn("⚠️ Music count güncellenemedi:", err?.message || err);
+ // Hata durumunda mevcut değeri koru, titreşimi önle
+ }
+}
diff --git a/client/src/stores/tvStore.js b/client/src/stores/tvStore.js
index 612fcab..4b3fbf9 100644
--- a/client/src/stores/tvStore.js
+++ b/client/src/stores/tvStore.js
@@ -2,15 +2,41 @@ import { writable } from "svelte/store";
import { apiFetch } from "../utils/api.js";
export const tvShowCount = writable(0);
+let requestSeq = 0;
+let lastValue = 0;
+let zeroTimer = null;
export async function refreshTvShowCount() {
+ const ticket = ++requestSeq;
try {
const resp = await apiFetch("/api/tvshows");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const list = await resp.json();
- tvShowCount.set(Array.isArray(list) ? list.length : 0);
+ if (ticket !== requestSeq) return;
+ const nextVal = Array.isArray(list) ? list.length : 0;
+ if (nextVal > 0) {
+ if (zeroTimer) {
+ clearTimeout(zeroTimer);
+ zeroTimer = null;
+ }
+ lastValue = nextVal;
+ tvShowCount.set(nextVal);
+ } else if (lastValue > 0) {
+ if (zeroTimer) clearTimeout(zeroTimer);
+ const zeroTicket = requestSeq;
+ zeroTimer = setTimeout(() => {
+ if (zeroTicket === requestSeq) {
+ lastValue = 0;
+ tvShowCount.set(0);
+ }
+ zeroTimer = null;
+ }, 500);
+ } else {
+ lastValue = 0;
+ tvShowCount.set(0);
+ }
} catch (err) {
console.warn("⚠️ TV show count güncellenemedi:", err?.message || err);
- tvShowCount.set(0);
+ // Hata durumunda mevcut değeri koru, titreşimi önle
}
}
diff --git a/docs/api-documentation.md b/docs/api-documentation.md
index a270a6b..ab1c2ff 100644
--- a/docs/api-documentation.md
+++ b/docs/api-documentation.md
@@ -650,4 +650,4 @@ curl -X GET http://localhost:3001/api/system/info
---
-*This API documentation provides comprehensive coverage of all available endpoints, WebSocket events, and usage patterns. For implementation details and examples, refer to the cross-referenced files and documentation.*
\ No newline at end of file
+*This API documentation provides comprehensive coverage of all available endpoints, WebSocket events, and usage patterns. For implementation details and examples, refer to the cross-referenced files and documentation.*
diff --git a/server/server.js b/server/server.js
index 7bbb33b..b67c768 100644
--- a/server/server.js
+++ b/server/server.js
@@ -764,9 +764,10 @@ async function finalizeYoutubeJob(job, exitCode) {
mediaInfo,
infoJson
);
- const payload = updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
- const mediaType = payload?.type || "video";
- const categories = payload?.categories || null;
+ const payloadWithThumb =
+ updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
+ const mediaType = payloadWithThumb?.type || "video";
+ const categories = payloadWithThumb?.categories || null;
upsertInfoFile(job.savePath, {
infoHash: job.id,
name: job.title,
@@ -5125,8 +5126,13 @@ app.get("/api/files", requireAuth, (req, res) => {
const seriesEpisodeInfo = relWithinRoot
? info.seriesEpisodes?.[relWithinRoot] || null
: null;
- const mediaCategory =
- fileMeta?.type || (relWithinRoot ? info.type : null) || null;
+ let mediaCategory = fileMeta?.type || null;
+ if (!mediaCategory) {
+ const canInheritFromInfo = !relWithinRoot || isVideo;
+ if (canInheritFromInfo && info.type) {
+ mediaCategory = info.type;
+ }
+ }
const isPrimaryVideo =
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
const displayName = entry.name;
@@ -6078,6 +6084,89 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
}
});
+function collectMusicEntries() {
+ const entries = [];
+ const dirEntries = fs
+ .readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
+ .filter((dirent) => dirent.isDirectory());
+
+ for (const dirent of dirEntries) {
+ const folder = sanitizeRelative(dirent.name);
+ if (!folder) continue;
+ // Klasörün tamamı çöpe taşınmışsa atla
+ if (isPathTrashed(folder, "", true)) continue;
+ const info = readInfoForRoot(folder) || {};
+ const files = info.files || {};
+ const fileKeys = Object.keys(files);
+ if (!fileKeys.length) continue;
+
+ let targetPath = info.primaryVideoPath;
+ if (targetPath && files[targetPath]?.type !== "music") {
+ targetPath = null;
+ }
+ if (!targetPath) {
+ targetPath =
+ fileKeys.find((key) => files[key]?.type === "music") || fileKeys[0];
+ }
+ if (!targetPath) continue;
+ const fileMeta = files[targetPath];
+ // Hedef dosya çöpteyse atla
+ if (isPathTrashed(folder, targetPath, false)) continue;
+ const mediaType = fileMeta?.type || info.type || null;
+ if (mediaType !== "music") continue;
+
+ const metadataPath = path.join(YT_DATA_ROOT, folder, "metadata.json");
+ let metadata = null;
+ if (fs.existsSync(metadataPath)) {
+ try {
+ metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
+ } catch (err) {
+ console.warn(`⚠️ YT metadata okunamadı (${metadataPath}): ${err.message}`);
+ }
+ }
+
+ const keysArray = fileKeys;
+ const fileIndex = Math.max(keysArray.indexOf(targetPath), 0);
+ const infoHash = info.infoHash || folder;
+ const title =
+ info.name || metadata?.title || path.basename(targetPath) || folder;
+ const thumbnail =
+ metadata?.thumbnail ||
+ (metadata ? `/yt-data/${folder}/thumbnail.jpg` : null);
+
+ entries.push({
+ id: `${folder}:${targetPath}`,
+ folder,
+ infoHash,
+ fileIndex,
+ filePath: targetPath,
+ title,
+ added: info.added || info.createdAt || null,
+ size: fileMeta?.size || 0,
+ url:
+ metadata?.url ||
+ fileMeta?.youtube?.url ||
+ fileMeta?.youtube?.videoId
+ ? `https://www.youtube.com/watch?v=${fileMeta.youtube.videoId}`
+ : null,
+ thumbnail,
+ categories: metadata?.categories || fileMeta?.categories || null
+ });
+ }
+ entries.sort((a, b) => (b.added || 0) - (a.added || 0));
+ return entries;
+}
+
+app.get("/api/music", requireAuth, (req, res) => {
+ try {
+ const entries = collectMusicEntries();
+ res.json(entries);
+ } catch (err) {
+ console.error("🎵 Music API error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
async function rebuildTvMetadata({ clearCache = false } = {}) {
if (!TVDB_API_KEY) {
throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil.");
@@ -6330,6 +6419,23 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
return streamLocalFile(absPath, range, res);
}
+ const info = readInfoForRoot(req.params.hash);
+ if (info && info.files) {
+ const fileKeys = Object.keys(info.files);
+ if (fileKeys.length) {
+ const idx = Number(req.query.index) || 0;
+ const targetKey = fileKeys[idx] || fileKeys[0];
+ const absPath = path.join(
+ DOWNLOAD_DIR,
+ req.params.hash,
+ targetKey.replace(/\\/g, "/")
+ );
+ if (fs.existsSync(absPath)) {
+ return streamLocalFile(absPath, range, res);
+ }
+ }
+ }
+
return res.status(404).end();
});