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

@@ -43,6 +43,17 @@ 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 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 [
THUMBNAIL_DIR,
@@ -494,6 +505,28 @@ function sanitizeRelative(relPath) {
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() {
if (resolvedYtDlpBinary) return resolvedYtDlpBinary;
const candidates = [
@@ -583,6 +616,7 @@ function launchYoutubeJob(job) {
"--write-thumbnail",
"--convert-thumbnails",
"jpg",
"--write-info-json",
job.url
];
const child = spawn(binary, args, {
@@ -697,6 +731,7 @@ async function finalizeYoutubeJob(job, exitCode) {
job.currentStage.done = true;
}
const infoJson = findYoutubeInfoJson(job.savePath);
const videoFile = findYoutubeVideoFile(job.savePath);
if (!videoFile) {
job.state = "error";
@@ -723,14 +758,23 @@ async function finalizeYoutubeJob(job, exitCode) {
job.progress = 1;
job.state = "completed";
await writeYoutubeMetadata(job, absVideo, mediaInfo);
updateYoutubeThumbnail(job);
const metadataPayload = await writeYoutubeMetadata(
job,
absVideo,
mediaInfo,
infoJson
);
const payload = updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
const mediaType = payload?.type || "video";
const categories = payload?.categories || null;
upsertInfoFile(job.savePath, {
infoHash: job.id,
name: job.title,
tracker: "youtube",
added: job.added,
folder: job.folderId,
type: mediaType,
categories,
files: {
[relativeName]: {
size: stats.size,
@@ -739,7 +783,9 @@ async function finalizeYoutubeJob(job, exitCode) {
youtube: {
url: job.url,
videoId: job.videoId
}
},
categories,
type: mediaType
}
},
primaryVideoPath: relativeName,
@@ -771,6 +817,17 @@ function findYoutubeVideoFile(savePath) {
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) {
const base = fileName.replace(path.extname(fileName), "");
const pattern = videoId ? new RegExp(`\\[${videoId}\\]`, "i") : null;
@@ -778,9 +835,29 @@ function deriveYoutubeTitle(fileName, videoId) {
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);
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 = {
id: job.id,
title: job.title,
@@ -789,16 +866,20 @@ async function writeYoutubeMetadata(job, videoPath, mediaInfo) {
added: job.added,
folderId: job.folderId,
file: job.files?.[0]?.name || null,
mediaInfo
mediaInfo,
type: derivedType,
categories,
ytMeta: infoJson || null
};
fs.writeFileSync(
path.join(targetDir, "metadata.json"),
JSON.stringify(payload, null, 2),
"utf-8"
);
return payload;
}
function updateYoutubeThumbnail(job) {
function updateYoutubeThumbnail(job, metadataPayload = null) {
const thumbs = fs
.readdirSync(job.savePath, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".jpg"));
@@ -813,6 +894,21 @@ function updateYoutubeThumbnail(job) {
} catch (err) {
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 } = {}) {
@@ -3975,7 +4071,14 @@ async function onTorrentDone({ torrent }) {
size: file.length,
extension: ext || null,
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);
@@ -4024,6 +4127,13 @@ async function onTorrentDone({ torrent }) {
matchedAt: Date.now()
}
};
perFileMetadata[normalizedRelPath].type = determineMediaType({
tracker: torrent.announce?.[0] || null,
movieMatch: null,
seriesEpisode: seriesEpisodes[normalizedRelPath],
categories: null,
relPath: normalizedRelPath
});
}
} catch (err) {
console.warn(
@@ -4072,8 +4182,8 @@ async function onTorrentDone({ torrent }) {
infoUpdate.files[bestVideoPath] = {
...entry,
movieMatch: ensuredMedia.metadata
? {
id: ensuredMedia.metadata.id ?? null,
? {
id: ensuredMedia.metadata.id ?? null,
title:
ensuredMedia.metadata.title ||
ensuredMedia.metadata.matched_title ||
@@ -4086,12 +4196,24 @@ async function onTorrentDone({ torrent }) {
poster: ensuredMedia.metadata.poster_path || null,
backdrop: ensuredMedia.metadata.backdrop_path || null,
cacheKey: ensuredMedia.cacheKey || null,
matchedAt: Date.now()
}
: entry.movieMatch
};
}
matchedAt: Date.now()
}
: 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);
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();
}
@@ -4929,6 +5064,7 @@ app.get("/api/files", requireAuth, (req, res) => {
tracker,
torrentName,
infoHash,
mediaCategory: dirInfo.type || null,
extension: null,
mediaInfo: null,
primaryVideoPath: null,
@@ -4989,6 +5125,8 @@ app.get("/api/files", requireAuth, (req, res) => {
const seriesEpisodeInfo = relWithinRoot
? info.seriesEpisodes?.[relWithinRoot] || null
: null;
const mediaCategory =
fileMeta?.type || (relWithinRoot ? info.type : null) || null;
const isPrimaryVideo =
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
const displayName = entry.name;
@@ -5006,6 +5144,7 @@ app.get("/api/files", requireAuth, (req, res) => {
tracker,
torrentName,
infoHash,
mediaCategory,
extension: extensionForFile,
mediaInfo: mediaInfoForFile,
primaryVideoPath: info.primaryVideoPath || null,