feat(rabbit): gelişmiş video oynatıcı ve gerçek zamanlı güncelleme desteği ekle

Video oynatıcıya özel kontroller, WebSocket desteği, arama fonksiyonu ve altyazı yükleme özelliği eklendi. Metadata yönetimi güçlendirildi, dosya silme ve geri yükleme işlemlerinde Rabbit listesi otomatik güncelleniyor.
This commit is contained in:
2025-12-27 22:52:43 +03:00
parent fddf369644
commit e49c2ac75d
4 changed files with 831 additions and 102 deletions

View File

@@ -1458,6 +1458,98 @@ function writeRabbitMetadata(job, absMedia, mediaInfo, infoJson) {
}
}
function removeRabbitMetadata(folderId) {
const safeFolder = sanitizeRelative(folderId);
if (!safeFolder) return false;
const targetDir = path.join(RABBIT_DATA_ROOT, safeFolder);
if (!fs.existsSync(targetDir)) return false;
try {
fs.rmSync(targetDir, { recursive: true, force: true });
return true;
} catch (err) {
console.warn("⚠️ Rabbit metadata silinemedi:", err.message);
return false;
}
}
function extractPornhubViewKey(folderId) {
if (!folderId || typeof folderId !== "string") return null;
if (!folderId.startsWith("ph_")) return null;
const parts = folderId.split("_");
if (parts.length < 2) return null;
return parts[1] || null;
}
async function rebuildRabbitMetadataForFolder(folderId) {
const safeFolder = sanitizeRelative(folderId);
if (!safeFolder) return false;
const rootDir = path.join(DOWNLOAD_DIR, safeFolder);
if (!fs.existsSync(rootDir)) return false;
const hasCompleteFlag = fs.existsSync(path.join(rootDir, ".ph_complete"));
const mediaFile = findYoutubeMediaFile(rootDir, false);
if (!mediaFile) return false;
const absMedia = path.join(rootDir, mediaFile);
if (!fs.existsSync(absMedia)) return false;
const infoJsonName = findYoutubeInfoJson(rootDir);
let infoJson = null;
if (infoJsonName) {
try {
infoJson = JSON.parse(fs.readFileSync(path.join(rootDir, infoJsonName), "utf-8"));
} catch (err) {
console.warn("⚠️ Rabbit info.json okunamadı:", err.message);
}
}
const extractor = String(infoJson?.extractor || infoJson?.extractor_key || "").toLowerCase();
const webpageUrl = String(infoJson?.webpage_url || infoJson?.original_url || "");
const isPornhub =
hasCompleteFlag ||
safeFolder.startsWith("ph_") ||
extractor.includes("pornhub") ||
webpageUrl.includes("pornhub.com");
if (!isPornhub) return false;
const viewKey = extractPornhubViewKey(safeFolder) || infoJson?.id || null;
const title = infoJson?.title || deriveYoutubeTitle(mediaFile, viewKey);
const url =
infoJson?.webpage_url ||
infoJson?.original_url ||
(viewKey ? `https://www.pornhub.com/view_video.php?viewkey=${viewKey}` : null);
let addedAt = Date.now();
if (Number.isFinite(infoJson?.timestamp)) {
addedAt = Number(infoJson.timestamp) * 1000;
} else {
try {
addedAt = fs.statSync(absMedia).mtimeMs || Date.now();
} catch (err) {
/* no-op */
}
}
const stats = fs.statSync(absMedia);
const mediaInfo = await extractMediaInfo(absMedia).catch(() => null);
const job = {
id: safeFolder,
folderId: safeFolder,
savePath: rootDir,
url,
added: addedAt,
title,
files: [
{
index: 0,
name: mediaFile.replace(/\\/g, "/"),
length: stats.size
}
]
};
writeRabbitMetadata(job, absMedia, mediaInfo, infoJson);
return true;
}
function countRabbitItems() {
try {
const entries = fs
@@ -4528,6 +4620,14 @@ function detectMediaFlagsForPath(info, relWithinRoot, isDirectory) {
const flags = { movies: false, tv: false };
if (!info || typeof info !== "object") return flags;
const infoFiles = info.files || {};
const isExternalMedia =
info.tracker === "youtube" ||
info.source === "youtube" ||
info.source === "pornhub" ||
Object.values(infoFiles).some((meta) => Boolean(meta?.youtube));
if (isExternalMedia) return flags;
const normalized = normalizeTrashPath(relWithinRoot);
const matchesPath = (candidate) => {
const normalizedCandidate = normalizeTrashPath(candidate);
@@ -4541,8 +4641,7 @@ function detectMediaFlagsForPath(info, relWithinRoot, isDirectory) {
return normalizedCandidate === normalized;
};
const files = info.files || {};
for (const [key, meta] of Object.entries(files)) {
for (const [key, meta] of Object.entries(infoFiles)) {
if (!meta) continue;
if (!matchesPath(key)) continue;
if (meta.movieMatch) flags.movies = true;
@@ -5518,6 +5617,10 @@ app.delete("/api/file", requireAuth, (req, res) => {
trashStateCache.delete(folderId);
}
if (folderId && removeRabbitMetadata(folderId)) {
broadcastRabbitCount();
}
if (folderId) {
let matchedInfoHash = null;
for (const [infoHash, entry] of torrents.entries()) {
@@ -5995,7 +6098,7 @@ app.get("/api/trash", requireAuth, (req, res) => {
});
// --- 🗑️ Çöpten geri yükleme API (.trash flag sistemi) ---
app.post("/api/trash/restore", requireAuth, (req, res) => {
app.post("/api/trash/restore", requireAuth, async (req, res) => {
try {
const { trashName } = req.body;
@@ -6021,6 +6124,9 @@ app.post("/api/trash/restore", requireAuth, (req, res) => {
console.log(`♻️ Öğe geri yüklendi: ${safeName}`);
broadcastFileUpdate(rootFolder);
if (await rebuildRabbitMetadataForFolder(rootFolder)) {
broadcastRabbitCount();
}
if (mediaFlags.movies || mediaFlags.tv) {
queueMediaRescan({
movies: mediaFlags.movies,
@@ -6463,9 +6569,22 @@ app.get("/api/rabbit", requireAuth, (req, res) => {
const items = [];
for (const folder of entries) {
const metaPath = path.join(RABBIT_DATA_ROOT, folder, "metadata.json");
const downloadRoot = path.join(DOWNLOAD_DIR, folder);
if (!fs.existsSync(metaPath) || !fs.existsSync(downloadRoot)) {
removeRabbitMetadata(folder);
continue;
}
if (!fs.existsSync(metaPath)) continue;
try {
const data = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
const fileRel = data?.file ? String(data.file) : null;
if (fileRel) {
const filePath = path.join(downloadRoot, fileRel);
if (!fs.existsSync(filePath)) {
removeRabbitMetadata(folder);
continue;
}
}
items.push({
id: data.id || folder,
title: data.title || folder,