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:
167
server/server.js
167
server/server.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user