diff --git a/client/src/App.svelte b/client/src/App.svelte index 7e35771..9711f52 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -17,6 +17,7 @@ import { refreshMovieCount } from "./stores/movieStore.js"; import { refreshTvShowCount } from "./stores/tvStore.js"; import { refreshMusicCount } from "./stores/musicStore.js"; + import { refreshRabbitCount } from "./stores/rabbitStore.js"; import { fetchTrashItems } from "./stores/trashStore.js"; import { setAvatarUrl } from "./stores/avatarStore.js"; @@ -35,6 +36,7 @@ refreshMovieCount(), refreshTvShowCount(), refreshMusicCount(), + refreshRabbitCount(), fetchTrashItems() ]); } catch (err) { @@ -86,6 +88,7 @@ refreshMovieCount(); refreshTvShowCount(); refreshMusicCount(); + refreshRabbitCount(); fetchTrashItems(); loadUserProfile(); const authToken = getAccessToken(); diff --git a/client/src/routes/Rabbit.svelte b/client/src/routes/Rabbit.svelte index b582d2d..931851d 100644 --- a/client/src/routes/Rabbit.svelte +++ b/client/src/routes/Rabbit.svelte @@ -1,7 +1,9 @@
+

Rabbit

-
@@ -75,12 +342,14 @@
{error}
{:else if items.length === 0}
Henüz içerik yok.
+ {:else if hasSearch && filteredItems.length === 0} +
Aramanıza uyan video bulunamadı.
{:else}
- {#each items as item (item.id)} + {#each filteredItems as item (item.id)}
playItem(item)}> {#if thumbUrl(item)} - {item.title} + {item.title} {:else}
{/if} @@ -89,156 +358,477 @@ {#if item.added}
{new Date(item.added).toLocaleString()}
{/if} + {#if item.size} +
{formatSize(item.size)}
+ {/if}
{/each} {/if} +
- {#if showPlayer && selected} -
(showPlayer = false)}> -
- -
{selected.title}
- {#if videoUrl(selected)} +{#if showPlayer && selectedVideo} + +{/if} diff --git a/client/src/stores/rabbitStore.js b/client/src/stores/rabbitStore.js index 78fa235..ac698fb 100644 --- a/client/src/stores/rabbitStore.js +++ b/client/src/stores/rabbitStore.js @@ -1,4 +1,5 @@ import { writable } from "svelte/store"; +import { apiFetch } from "../utils/api.js"; const countStore = writable(0); export const rabbitCount = countStore; @@ -6,3 +7,19 @@ export const rabbitCount = countStore; export function setRabbitCount(count) { countStore.set(Number(count) || 0); } + +// Rabbit listesini yenile +export async function refreshRabbitCount() { + try { + const resp = await apiFetch("/api/rabbit"); + if (!resp.ok) { + console.warn("Rabbit listesi yenilenemedi:", resp.status); + return; + } + const data = await resp.json(); + const count = data?.items?.length || 0; + setRabbitCount(count); + } catch (err) { + console.warn("Rabbit sayısı yenilenemedi:", err?.message || err); + } +} diff --git a/server/server.js b/server/server.js index 18d333c..99b36f6 100644 --- a/server/server.js +++ b/server/server.js @@ -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,