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:
@@ -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}
|
||||||
|
|||||||
153
server/server.js
153
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 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,6 +4200,18 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user