Youtube dan indirilen videoların kategorisi müzik mi değil mi tespit edilebiliyor! İkonlar buna göre gösteriliyor.

This commit is contained in:
2025-11-30 21:05:57 +03:00
parent 4ab37005cf
commit cd36080b3a
2 changed files with 180 additions and 25 deletions

View File

@@ -85,6 +85,7 @@
return { return {
...file, ...file,
isDirectory, isDirectory,
mediaCategory: file.mediaCategory || null,
hiddenRoot, hiddenRoot,
originalSegments, originalSegments,
displaySegments, displaySegments,
@@ -97,7 +98,13 @@
function buildDirectoryEntries(fileList) { function buildDirectoryEntries(fileList) {
const directories = new Map(); const directories = new Map();
const ensureDirectoryEntry = (key, displayName, parentDisplayPath, originalPath) => { const ensureDirectoryEntry = (
key,
displayName,
parentDisplayPath,
originalPath,
mediaCategory
) => {
if (!key) return; if (!key) return;
if (!directories.has(key)) { if (!directories.has(key)) {
directories.set(key, { directories.set(key, {
@@ -108,6 +115,7 @@
parentDisplayPath, parentDisplayPath,
originalPaths: new Set(), originalPaths: new Set(),
isDirectory: true, isDirectory: true,
mediaCategory: mediaCategory || null
}); });
} }
if (originalPath) { if (originalPath) {
@@ -128,7 +136,13 @@
const displayPath = segments.join("/"); const displayPath = segments.join("/");
const parentDisplayPath = segments.slice(0, -1).join("/"); const parentDisplayPath = segments.slice(0, -1).join("/");
const displayName = file.displayName || segments[segments.length - 1] || displayPath; const displayName = file.displayName || segments[segments.length - 1] || displayPath;
ensureDirectoryEntry(displayPath, displayName, parentDisplayPath, fullOriginalPath); ensureDirectoryEntry(
displayPath,
displayName,
parentDisplayPath,
fullOriginalPath,
file.mediaCategory
);
} }
if (segments.length <= 1) continue; if (segments.length <= 1) continue;
@@ -2189,16 +2203,18 @@
</div> </div>
</div> </div>
<div class="media-type-icon"> <div class="media-type-icon">
{#if entry.type?.startsWith("video/")} {#if entry.type?.startsWith("image/")}
{#if entry.seriesEpisode || (entry.seriesEpisodes && Object.keys(entry.seriesEpisodes).length > 0)}
<i class="fa-solid fa-tv"></i>
{:else if entry.movieMatch}
<i class="fa-solid fa-film"></i>
{:else}
<i class="fa-solid fa-ban"></i>
{/if}
{:else if entry.type?.startsWith("image/")}
<i class="fa-solid fa-image"></i> <i class="fa-solid fa-image"></i>
{:else if entry.mediaCategory === "tv"}
<i class="fa-solid fa-tv"></i>
{:else if entry.mediaCategory === "movie"}
<i class="fa-solid fa-film"></i>
{:else if entry.mediaCategory === "music"}
<i class="fa-solid fa-music"></i>
{:else if entry.type?.startsWith("video/")}
<i class="fa-solid fa-play"></i>
{:else}
<i class="fa-solid fa-file"></i>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@@ -43,6 +43,17 @@ const IMAGE_THUMB_ROOT = path.join(THUMBNAIL_DIR, "images");
const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data"); const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data"); const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data"); const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
const MUSIC_EXTENSIONS = new Set([
".mp3",
".m4a",
".aac",
".flac",
".wav",
".ogg",
".oga",
".opus",
".mka"
]);
for (const dir of [ for (const dir of [
THUMBNAIL_DIR, THUMBNAIL_DIR,
@@ -494,6 +505,28 @@ function sanitizeRelative(relPath) {
return relPath.replace(/^[\\/]+/, ""); return relPath.replace(/^[\\/]+/, "");
} }
function determineMediaType({
tracker,
movieMatch,
seriesEpisode,
categories,
relPath
}) {
if (seriesEpisode) return "tv";
if (movieMatch) return "movie";
if (
Array.isArray(categories) &&
categories.some((cat) => String(cat).toLowerCase() === "music")
) {
return "music";
}
if (relPath) {
const ext = path.extname(relPath).toLowerCase();
if (MUSIC_EXTENSIONS.has(ext)) return "music";
}
return "video";
}
function getYtDlpBinary() { function getYtDlpBinary() {
if (resolvedYtDlpBinary) return resolvedYtDlpBinary; if (resolvedYtDlpBinary) return resolvedYtDlpBinary;
const candidates = [ const candidates = [
@@ -583,6 +616,7 @@ function launchYoutubeJob(job) {
"--write-thumbnail", "--write-thumbnail",
"--convert-thumbnails", "--convert-thumbnails",
"jpg", "jpg",
"--write-info-json",
job.url job.url
]; ];
const child = spawn(binary, args, { const child = spawn(binary, args, {
@@ -697,6 +731,7 @@ async function finalizeYoutubeJob(job, exitCode) {
job.currentStage.done = true; job.currentStage.done = true;
} }
const infoJson = findYoutubeInfoJson(job.savePath);
const videoFile = findYoutubeVideoFile(job.savePath); const videoFile = findYoutubeVideoFile(job.savePath);
if (!videoFile) { if (!videoFile) {
job.state = "error"; job.state = "error";
@@ -723,14 +758,23 @@ async function finalizeYoutubeJob(job, exitCode) {
job.progress = 1; job.progress = 1;
job.state = "completed"; job.state = "completed";
await writeYoutubeMetadata(job, absVideo, mediaInfo); const metadataPayload = await writeYoutubeMetadata(
updateYoutubeThumbnail(job); job,
absVideo,
mediaInfo,
infoJson
);
const payload = updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
const mediaType = payload?.type || "video";
const categories = payload?.categories || null;
upsertInfoFile(job.savePath, { upsertInfoFile(job.savePath, {
infoHash: job.id, infoHash: job.id,
name: job.title, name: job.title,
tracker: "youtube", tracker: "youtube",
added: job.added, added: job.added,
folder: job.folderId, folder: job.folderId,
type: mediaType,
categories,
files: { files: {
[relativeName]: { [relativeName]: {
size: stats.size, size: stats.size,
@@ -739,7 +783,9 @@ async function finalizeYoutubeJob(job, exitCode) {
youtube: { youtube: {
url: job.url, url: job.url,
videoId: job.videoId videoId: job.videoId
} },
categories,
type: mediaType
} }
}, },
primaryVideoPath: relativeName, primaryVideoPath: relativeName,
@@ -771,6 +817,17 @@ function findYoutubeVideoFile(savePath) {
return videos[0]; return videos[0];
} }
function findYoutubeInfoJson(savePath) {
const entries = fs.readdirSync(savePath, { withFileTypes: true });
const jsons = entries
.filter((entry) =>
entry.isFile() && entry.name.toLowerCase().endsWith(".info.json")
)
.map((entry) => entry.name)
.sort();
return jsons[0] || null;
}
function deriveYoutubeTitle(fileName, videoId) { function deriveYoutubeTitle(fileName, videoId) {
const base = fileName.replace(path.extname(fileName), ""); const base = fileName.replace(path.extname(fileName), "");
const pattern = videoId ? new RegExp(`\\[${videoId}\\]`, "i") : null; const pattern = videoId ? new RegExp(`\\[${videoId}\\]`, "i") : null;
@@ -778,9 +835,29 @@ function deriveYoutubeTitle(fileName, videoId) {
return cleaned.replace(/[-_.]+$/g, "").trim() || base; return cleaned.replace(/[-_.]+$/g, "").trim() || base;
} }
async function writeYoutubeMetadata(job, videoPath, mediaInfo) { async function writeYoutubeMetadata(job, videoPath, mediaInfo, infoJsonFile) {
const targetDir = path.join(YT_DATA_ROOT, job.folderId); const targetDir = path.join(YT_DATA_ROOT, job.folderId);
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
let infoJson = null;
if (infoJsonFile) {
try {
infoJson = JSON.parse(
fs.readFileSync(path.join(job.savePath, infoJsonFile), "utf-8")
);
} catch (err) {
console.warn("YouTube info.json okunamadı:", err.message);
}
}
const categories = Array.isArray(infoJson?.categories)
? infoJson.categories
: null;
const derivedType = determineMediaType({
tracker: "youtube",
movieMatch: null,
seriesEpisode: null,
categories,
relPath: job.files?.[0]?.name || null
});
const payload = { const payload = {
id: job.id, id: job.id,
title: job.title, title: job.title,
@@ -789,16 +866,20 @@ async function writeYoutubeMetadata(job, videoPath, mediaInfo) {
added: job.added, added: job.added,
folderId: job.folderId, folderId: job.folderId,
file: job.files?.[0]?.name || null, file: job.files?.[0]?.name || null,
mediaInfo mediaInfo,
type: derivedType,
categories,
ytMeta: infoJson || null
}; };
fs.writeFileSync( fs.writeFileSync(
path.join(targetDir, "metadata.json"), path.join(targetDir, "metadata.json"),
JSON.stringify(payload, null, 2), JSON.stringify(payload, null, 2),
"utf-8" "utf-8"
); );
return payload;
} }
function updateYoutubeThumbnail(job) { function updateYoutubeThumbnail(job, metadataPayload = null) {
const thumbs = fs const thumbs = fs
.readdirSync(job.savePath, { withFileTypes: true }) .readdirSync(job.savePath, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".jpg")); .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".jpg"));
@@ -813,6 +894,21 @@ function updateYoutubeThumbnail(job) {
} catch (err) { } catch (err) {
console.warn("Thumbnail kopyalanamadı:", err.message); console.warn("Thumbnail kopyalanamadı:", err.message);
} }
try {
const metaPath = path.join(YT_DATA_ROOT, job.folderId, "metadata.json");
let payload = metadataPayload;
if (!payload && fs.existsSync(metaPath)) {
payload = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
}
if (payload) {
payload.thumbnail = job.thumbnail;
fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), "utf-8");
return payload;
}
} catch (err) {
console.warn("YT metadata güncellenemedi:", err.message);
}
return metadataPayload || null;
} }
function removeYoutubeJob(jobId, { removeFiles = true } = {}) { function removeYoutubeJob(jobId, { removeFiles = true } = {}) {
@@ -3975,7 +4071,14 @@ async function onTorrentDone({ torrent }) {
size: file.length, size: file.length,
extension: ext || null, extension: ext || null,
mimeType, mimeType,
mediaInfo: metaInfo mediaInfo: metaInfo,
type: determineMediaType({
tracker: torrent.announce?.[0] || null,
movieMatch: null,
seriesEpisode: null,
categories: null,
relPath: normalizedRelPath
})
}; };
const seriesInfo = parseSeriesInfo(file.name); const seriesInfo = parseSeriesInfo(file.name);
@@ -4024,6 +4127,13 @@ async function onTorrentDone({ torrent }) {
matchedAt: Date.now() matchedAt: Date.now()
} }
}; };
perFileMetadata[normalizedRelPath].type = determineMediaType({
tracker: torrent.announce?.[0] || null,
movieMatch: null,
seriesEpisode: seriesEpisodes[normalizedRelPath],
categories: null,
relPath: normalizedRelPath
});
} }
} catch (err) { } catch (err) {
console.warn( console.warn(
@@ -4090,8 +4200,20 @@ async function onTorrentDone({ torrent }) {
} }
: entry.movieMatch : entry.movieMatch
}; };
const movieType = determineMediaType({
tracker: torrent.announce?.[0] || null,
movieMatch: ensuredMedia.metadata,
seriesEpisode: seriesEpisodes[bestVideoPath] || null,
categories: null,
relPath: bestVideoPath
});
perFileMetadata[bestVideoPath] = {
...(perFileMetadata[bestVideoPath] || {}),
type: movieType
};
infoUpdate.files[bestVideoPath].type = movieType;
} }
} }
upsertInfoFile(entry.savePath, infoUpdate); upsertInfoFile(entry.savePath, infoUpdate);
broadcastFileUpdate(rootFolder); broadcastFileUpdate(rootFolder);
@@ -4112,6 +4234,19 @@ async function onTorrentDone({ torrent }) {
} }
} }
if (bestVideoPath && infoUpdate.files && infoUpdate.files[bestVideoPath]) {
const rootType = determineMediaType({
tracker: torrent.announce?.[0] || null,
movieMatch: infoUpdate.files[bestVideoPath].movieMatch || null,
seriesEpisode: seriesEpisodes[bestVideoPath] || null,
categories: null,
relPath: bestVideoPath
});
infoUpdate.type = rootType;
infoUpdate.files[bestVideoPath].type =
infoUpdate.files[bestVideoPath].type || rootType;
}
broadcastSnapshot(); broadcastSnapshot();
} }
@@ -4929,6 +5064,7 @@ app.get("/api/files", requireAuth, (req, res) => {
tracker, tracker,
torrentName, torrentName,
infoHash, infoHash,
mediaCategory: dirInfo.type || null,
extension: null, extension: null,
mediaInfo: null, mediaInfo: null,
primaryVideoPath: null, primaryVideoPath: null,
@@ -4989,6 +5125,8 @@ app.get("/api/files", requireAuth, (req, res) => {
const seriesEpisodeInfo = relWithinRoot const seriesEpisodeInfo = relWithinRoot
? info.seriesEpisodes?.[relWithinRoot] || null ? info.seriesEpisodes?.[relWithinRoot] || null
: null; : null;
const mediaCategory =
fileMeta?.type || (relWithinRoot ? info.type : null) || null;
const isPrimaryVideo = const isPrimaryVideo =
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot; !!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
const displayName = entry.name; const displayName = entry.name;
@@ -5006,6 +5144,7 @@ app.get("/api/files", requireAuth, (req, res) => {
tracker, tracker,
torrentName, torrentName,
infoHash, infoHash,
mediaCategory,
extension: extensionForFile, extension: extensionForFile,
mediaInfo: mediaInfoForFile, mediaInfo: mediaInfoForFile,
primaryVideoPath: info.primaryVideoPath || null, primaryVideoPath: info.primaryVideoPath || null,