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:
@@ -17,6 +17,7 @@
|
|||||||
import { refreshMovieCount } from "./stores/movieStore.js";
|
import { refreshMovieCount } from "./stores/movieStore.js";
|
||||||
import { refreshTvShowCount } from "./stores/tvStore.js";
|
import { refreshTvShowCount } from "./stores/tvStore.js";
|
||||||
import { refreshMusicCount } from "./stores/musicStore.js";
|
import { refreshMusicCount } from "./stores/musicStore.js";
|
||||||
|
import { refreshRabbitCount } from "./stores/rabbitStore.js";
|
||||||
import { fetchTrashItems } from "./stores/trashStore.js";
|
import { fetchTrashItems } from "./stores/trashStore.js";
|
||||||
import { setAvatarUrl } from "./stores/avatarStore.js";
|
import { setAvatarUrl } from "./stores/avatarStore.js";
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
refreshMovieCount(),
|
refreshMovieCount(),
|
||||||
refreshTvShowCount(),
|
refreshTvShowCount(),
|
||||||
refreshMusicCount(),
|
refreshMusicCount(),
|
||||||
|
refreshRabbitCount(),
|
||||||
fetchTrashItems()
|
fetchTrashItems()
|
||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -86,6 +88,7 @@
|
|||||||
refreshMovieCount();
|
refreshMovieCount();
|
||||||
refreshTvShowCount();
|
refreshTvShowCount();
|
||||||
refreshMusicCount();
|
refreshMusicCount();
|
||||||
|
refreshRabbitCount();
|
||||||
fetchTrashItems();
|
fetchTrashItems();
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
const authToken = getAccessToken();
|
const authToken = getAccessToken();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { apiFetch, withToken, API } from "../utils/api.js";
|
import { apiFetch, withToken, API } from "../utils/api.js";
|
||||||
import { setRabbitCount } from "../stores/rabbitStore.js";
|
import { setRabbitCount } from "../stores/rabbitStore.js";
|
||||||
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
import { getAccessToken } from "../utils/api.js";
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
@@ -9,15 +11,33 @@
|
|||||||
let selected = null;
|
let selected = null;
|
||||||
let showPlayer = false;
|
let showPlayer = false;
|
||||||
|
|
||||||
|
// Video player değişkenleri
|
||||||
|
let playerItems = [];
|
||||||
|
let selectedVideo = null;
|
||||||
|
let videoEl;
|
||||||
|
let isPlaying = false;
|
||||||
|
let currentTime = 0;
|
||||||
|
let duration = 0;
|
||||||
|
let volume = 1;
|
||||||
|
let subtitleURL = null;
|
||||||
|
let subtitleLabel = "Custom Subtitles";
|
||||||
|
let currentIndex = -1;
|
||||||
|
let searchTerm = "";
|
||||||
|
let hasSearch = false;
|
||||||
|
let filteredItems = [];
|
||||||
|
|
||||||
|
$: selectedName = selectedVideo?.name ?? "";
|
||||||
|
$: encName = selectedName ? encodeURIComponent(selectedName) : "";
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/rabbit");
|
const resp = await apiFetch("/api/rabbit");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
items = data?.items || [];
|
items = data?.items || [];
|
||||||
setRabbitCount(items.length);
|
setRabbitCount(items.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Rabbit listesi alınamadı";
|
error = err?.message || "Rabbit listesi alınamadı";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -25,7 +45,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
let wsRabbit = null;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
|
||||||
|
// WebSocket bağlantısı - fileUpdate mesajlarını dinle
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
const wsUrl = `${API.replace("http", "ws")}?token=${token}`;
|
||||||
|
try {
|
||||||
|
wsRabbit = new WebSocket(wsUrl);
|
||||||
|
wsRabbit.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.type === "fileUpdate") {
|
||||||
|
console.log("📹 Rabbit fileUpdate mesajı alındı, listeyi yeniliyor");
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("WS mesajı çözümlenemedi:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wsRabbit.onerror = () => {
|
||||||
|
console.warn("WS bağlantısı başarısız (Rabbit)");
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("WS bağlantısı kurulamadı (Rabbit):", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKey = (event) => {
|
||||||
|
if (!showPlayer) return;
|
||||||
|
if (isEditableTarget(event.target)) return;
|
||||||
|
|
||||||
|
const isCmd = event.metaKey || event.ctrlKey;
|
||||||
|
if (isCmd && event.key.toLowerCase() === "a") {
|
||||||
|
if (isEditableTarget(event.target)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
closePlayer();
|
||||||
|
} else if (event.key === " " || event.key === "Spacebar") {
|
||||||
|
event.preventDefault();
|
||||||
|
togglePlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKey);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKey);
|
||||||
|
if (wsRabbit) {
|
||||||
|
try {
|
||||||
|
wsRabbit.close();
|
||||||
|
} catch (err) {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
wsRabbit = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function thumbUrl(item) {
|
function thumbUrl(item) {
|
||||||
if (!item.thumbnail) return null;
|
if (!item.thumbnail) return null;
|
||||||
@@ -33,39 +114,225 @@
|
|||||||
return withToken(url);
|
return withToken(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoUrl(item) {
|
function mapRabbitToPlayerItem(item) {
|
||||||
if (!item?.file || !item?.folderId) return null;
|
if (!item?.file) {
|
||||||
const safeFolder = encodeURIComponent(item.folderId.trim());
|
console.warn("⚠️ Rabbit item eksik (file yok):", item);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// API'den gelen id alanını folderId olarak kullan
|
||||||
|
const folderId = item.id || item.folderId;
|
||||||
|
if (!folderId) {
|
||||||
|
console.warn("⚠️ Rabbit item eksik (id/folderId yok):", item);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const safeFolder = encodeURIComponent(folderId.trim());
|
||||||
const segments = String(item.file)
|
const segments = String(item.file)
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((seg) => encodeURIComponent(seg.trim()));
|
.map((seg) => encodeURIComponent(seg.trim()));
|
||||||
const relPath = segments.join("/");
|
const fileName = segments.join("/");
|
||||||
const url = `${API}/downloads/${safeFolder}/${relPath}`;
|
// folderId/filename formatında path oluştur
|
||||||
|
const name = `${safeFolder}/${fileName}`;
|
||||||
// Debug: console'a URL yazdır
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
||||||
console.log("🎥 Video URL Debug:", {
|
const inferredType = ext ? `video/${ext}` : "video/mp4";
|
||||||
folderId: item.folderId,
|
const size = Number(item.size) || null;
|
||||||
file: item.file,
|
console.log("📹 Rabbit player item oluşturuldu:", { id: item.id, folderId, file: item.file, name });
|
||||||
safeFolder,
|
return {
|
||||||
relPath,
|
name,
|
||||||
finalUrl: withToken(url)
|
type: inferredType.startsWith("video/") ? inferredType : "video/mp4",
|
||||||
});
|
size,
|
||||||
|
item
|
||||||
return withToken(url);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: playerItems = items.map(mapRabbitToPlayerItem).filter(Boolean);
|
||||||
|
|
||||||
|
$: searchTerm = searchTerm;
|
||||||
|
$: hasSearch = searchTerm.trim().length > 0;
|
||||||
|
$: filteredItems = (() => {
|
||||||
|
const query = searchTerm.trim().toLowerCase();
|
||||||
|
if (!query) return items;
|
||||||
|
return items.filter((item) => {
|
||||||
|
const fields = [
|
||||||
|
item.title,
|
||||||
|
item.folderId,
|
||||||
|
item.file
|
||||||
|
];
|
||||||
|
return fields
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
function playItem(item) {
|
function playItem(item) {
|
||||||
selected = item;
|
if (!item) return;
|
||||||
|
openPlayerForItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPlayerForItem(item) {
|
||||||
|
if (!playerItems.length) return;
|
||||||
|
const index = playerItems.findIndex(
|
||||||
|
(playerItem) => playerItem.item.id === item.id
|
||||||
|
);
|
||||||
|
if (index === -1) return;
|
||||||
|
await openVideoAtIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openVideoAtIndex(index) {
|
||||||
|
if (!playerItems.length) return;
|
||||||
|
const safeIndex =
|
||||||
|
((index % playerItems.length) + playerItems.length) % playerItems.length;
|
||||||
|
stopCurrentVideo();
|
||||||
|
currentIndex = safeIndex;
|
||||||
|
selectedVideo = playerItems[safeIndex];
|
||||||
|
subtitleURL = null;
|
||||||
|
await tick();
|
||||||
showPlayer = true;
|
showPlayer = true;
|
||||||
|
isPlaying = false;
|
||||||
|
currentTime = 0;
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCurrentVideo() {
|
||||||
|
if (videoEl) {
|
||||||
|
try {
|
||||||
|
videoEl.pause();
|
||||||
|
videoEl.src = "";
|
||||||
|
videoEl.load();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Video stop error:", err?.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePlayer() {
|
||||||
|
stopCurrentVideo();
|
||||||
|
showPlayer = false;
|
||||||
|
selectedVideo = null;
|
||||||
|
subtitleURL = null;
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoURL() {
|
||||||
|
if (!selectedName) return "";
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
return `${API}/media/${encName}?token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePlay() {
|
||||||
|
if (!videoEl) return;
|
||||||
|
if (videoEl.paused) {
|
||||||
|
try {
|
||||||
|
await videoEl.play();
|
||||||
|
isPlaying = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Play rejected:", err?.message || err);
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
videoEl.pause();
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
currentTime = videoEl?.currentTime || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDuration() {
|
||||||
|
duration = videoEl?.duration || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seekVideo(event) {
|
||||||
|
if (!videoEl) return;
|
||||||
|
const newTime = parseFloat(event.target.value);
|
||||||
|
if (Number.isFinite(newTime) && Math.abs(videoEl.currentTime - newTime) > 0.2) {
|
||||||
|
videoEl.currentTime = newTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeVolume(event) {
|
||||||
|
if (!videoEl) return;
|
||||||
|
const nextVolume = parseFloat(event.target.value);
|
||||||
|
videoEl.volume = nextVolume;
|
||||||
|
event.target.style.setProperty("--fill", (nextVolume || 0) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (!videoEl) return;
|
||||||
|
if (document.fullscreenElement) document.exitFullscreen();
|
||||||
|
else videoEl.requestFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (!bytes) return "0 MB";
|
||||||
|
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
|
||||||
|
if (bytes < 1e9) return (bytes / 1e6).toFixed(1) + " MB";
|
||||||
|
return (bytes / 1e9).toFixed(2) + " GB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const m = Math.floor(seconds / 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
|
const s = Math.floor(seconds % 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
|
return `${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubtitleUpload(e) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const ext = file.name.split(".").pop().toLowerCase();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const content =
|
||||||
|
typeof ev.target.result === "string"
|
||||||
|
? ev.target.result
|
||||||
|
: decoder.decode(ev.target.result);
|
||||||
|
if (ext === "srt") {
|
||||||
|
const vttText =
|
||||||
|
"\uFEFFWEBVTT\n\n" + content.replace(/\r+/g, "").replace(/,/g, ".");
|
||||||
|
const blob = new Blob([vttText], { type: "text/vtt;charset=utf-8" });
|
||||||
|
subtitleURL = URL.createObjectURL(blob);
|
||||||
|
} else if (ext === "vtt") {
|
||||||
|
const blob = new Blob([content], { type: "text/vtt;charset=utf-8" });
|
||||||
|
subtitleURL = URL.createObjectURL(blob);
|
||||||
|
} else {
|
||||||
|
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEXT_INPUT_TYPES = new Set([
|
||||||
|
"text",
|
||||||
|
"search",
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"number",
|
||||||
|
"url",
|
||||||
|
"tel"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isEditableTarget(target) {
|
||||||
|
if (!target) return false;
|
||||||
|
const tag = target.tagName;
|
||||||
|
const type = target.type?.toLowerCase();
|
||||||
|
if (tag === "TEXTAREA") return true;
|
||||||
|
if (tag === "INPUT" && TEXT_INPUT_TYPES.has(type)) return true;
|
||||||
|
return Boolean(target.isContentEditable);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rabbit-page">
|
<section class="rabbit-page">
|
||||||
|
<div class="section-accent"></div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Rabbit</h2>
|
<h2>Rabbit</h2>
|
||||||
<button class="btn" on:click={load} disabled={loading}>
|
<button class="refresh-btn" on:click={load} disabled={loading}>
|
||||||
<i class="fa-solid fa-rotate"></i> Yenile
|
<i class="fa-solid fa-rotate"></i> {loading ? "Yükleniyor..." : "Yenile"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,12 +342,14 @@
|
|||||||
<div class="state error">{error}</div>
|
<div class="state error">{error}</div>
|
||||||
{:else if items.length === 0}
|
{:else if items.length === 0}
|
||||||
<div class="state">Henüz içerik yok.</div>
|
<div class="state">Henüz içerik yok.</div>
|
||||||
|
{:else if hasSearch && filteredItems.length === 0}
|
||||||
|
<div class="state">Aramanıza uyan video bulunamadı.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each items as item (item.id)}
|
{#each filteredItems as item (item.id)}
|
||||||
<div class="card" on:click={() => playItem(item)}>
|
<div class="card" on:click={() => playItem(item)}>
|
||||||
{#if thumbUrl(item)}
|
{#if thumbUrl(item)}
|
||||||
<img class="thumb" src={thumbUrl(item)} alt={item.title} />
|
<img class="thumb" src={thumbUrl(item)} alt={item.title} loading="lazy" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="thumb placeholder"><i class="fa-regular fa-image"></i></div>
|
<div class="thumb placeholder"><i class="fa-regular fa-image"></i></div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -89,156 +358,477 @@
|
|||||||
{#if item.added}
|
{#if item.added}
|
||||||
<div class="meta">{new Date(item.added).toLocaleString()}</div>
|
<div class="meta">{new Date(item.added).toLocaleString()}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if item.size}
|
||||||
|
<div class="meta">{formatSize(item.size)}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
{#if showPlayer && selected}
|
{#if showPlayer && selectedVideo}
|
||||||
<div class="player" on:click={() => (showPlayer = false)}>
|
<div class="modal-overlay" on:click={closePlayer}>
|
||||||
<div class="player-content" on:click|stopPropagation>
|
<button class="global-close-btn" on:click|stopPropagation={closePlayer}>
|
||||||
<button class="player-close" on:click={() => (showPlayer = false)}>Kapat</button>
|
✕
|
||||||
<div class="player-title">{selected.title}</div>
|
</button>
|
||||||
{#if videoUrl(selected)}
|
<div class="modal-content" on:click|stopPropagation>
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="video-title">{selectedVideo?.item?.title || cleanFileName(selectedName)}</div>
|
||||||
|
<div class="video-meta">
|
||||||
|
<span>{formatSize(selectedVideo.size || 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-player">
|
||||||
|
{#key encName}
|
||||||
<video
|
<video
|
||||||
controls
|
bind:this={videoEl}
|
||||||
autoplay
|
src={getVideoURL()}
|
||||||
src={videoUrl(selected)}
|
class="video-element"
|
||||||
on:error={(e) => {
|
playsinline
|
||||||
console.error("🎥 Video Error:", e.target.error);
|
on:timeupdate={updateProgress}
|
||||||
console.log("🎥 Failed src:", videoUrl(selected));
|
on:loadedmetadata={async () => {
|
||||||
|
isPlaying = false;
|
||||||
|
currentTime = 0;
|
||||||
|
updateDuration();
|
||||||
|
const slider = document.querySelector(".volume-slider");
|
||||||
|
if (slider) {
|
||||||
|
slider.value = volume;
|
||||||
|
slider.style.setProperty("--fill", slider.value * 100);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await videoEl.play();
|
||||||
|
isPlaying = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Autoplay engellendi:", err?.message || err);
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
on:loadeddata={() => {
|
on:ended={() => (isPlaying = false)}
|
||||||
console.log("🎥 Video loaded successfully");
|
>
|
||||||
}}
|
{#if subtitleURL}
|
||||||
></video>
|
<track
|
||||||
{:else}
|
kind="subtitles"
|
||||||
<div class="state error">Video yolu bulunamadı</div>
|
src={subtitleURL}
|
||||||
{/if}
|
label={subtitleLabel}
|
||||||
|
default
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</video>
|
||||||
|
{/key}
|
||||||
|
<div class="controls">
|
||||||
|
<div class="top-controls">
|
||||||
|
<button class="control-btn" on:click={togglePlay}>
|
||||||
|
{#if isPlaying}
|
||||||
|
<i class="fa-solid fa-pause"></i>
|
||||||
|
{:else}
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<div class="right-controls">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
bind:value={volume}
|
||||||
|
on:input={changeVolume}
|
||||||
|
class="volume-slider"
|
||||||
|
/>
|
||||||
|
<button class="control-btn" on:click={toggleFullscreen}>
|
||||||
|
<i class="fa-solid fa-expand"></i>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getVideoURL()}
|
||||||
|
download={selectedName ? selectedName.split("/").pop() : undefined}
|
||||||
|
class="control-btn"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
</a>
|
||||||
|
<label class="control-btn subtitle-icon" title="Add subtitles">
|
||||||
|
<i class="fa-solid fa-closed-captioning"></i>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".srt,.vtt"
|
||||||
|
on:change={handleSubtitleUpload}
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-controls">
|
||||||
|
<span class="time">
|
||||||
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={duration}
|
||||||
|
step="0.1"
|
||||||
|
bind:value={currentTime}
|
||||||
|
on:input={seekVideo}
|
||||||
|
class="progress-slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</section>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.rabbit-page {
|
.rabbit-page {
|
||||||
padding: 16px;
|
padding: 20px 26px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
height: 2px;
|
||||||
|
width: calc(100% + 52px);
|
||||||
|
background: var(--yellow, #f5b333);
|
||||||
|
border-radius: 999px;
|
||||||
|
margin: -20px -26px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
.btn {
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: #2e2e2e;
|
||||||
|
color: #f5f5f5;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #dcdcdc;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f7f7f7;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:not(:disabled):hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
gap: 12px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #f5f5f5;
|
background: #f7f7f7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
transition: box-shadow 0.18s ease;
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
background 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: #cfcfcf;
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180px;
|
aspect-ratio: 16 / 9;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb.placeholder {
|
.thumb.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
|
background: #ececec;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
padding: 8px;
|
padding: 10px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
line-height: 1.2;
|
line-height: 1.3;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #777;
|
||||||
}
|
}
|
||||||
.player {
|
|
||||||
|
.state {
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #7a7a7a;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state.error {
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Player Styles */
|
||||||
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.65);
|
backdrop-filter: blur(4px);
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 6000;
|
||||||
}
|
}
|
||||||
.player-content {
|
|
||||||
background: #111;
|
.modal-content {
|
||||||
padding: 12px;
|
width: 70%;
|
||||||
border-radius: 10px;
|
height: 70%;
|
||||||
max-width: 900px;
|
background: #1a1a1a;
|
||||||
width: 90%;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
.player video {
|
|
||||||
width: 100%;
|
.global-close-btn {
|
||||||
max-height: 520px;
|
position: absolute;
|
||||||
border-radius: 8px;
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-close-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-meta {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-player {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 0;
|
||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
.player-title {
|
|
||||||
font-weight: 700;
|
.video-element {
|
||||||
font-size: 15px;
|
flex: 1 1 auto;
|
||||||
color: #fff;
|
width: 100%;
|
||||||
}
|
height: auto;
|
||||||
.player-close {
|
max-height: 100%;
|
||||||
align-self: flex-end;
|
min-height: 0;
|
||||||
background: #fff;
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
outline: none;
|
||||||
padding: 6px 10px;
|
}
|
||||||
|
|
||||||
|
.video-element:focus {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
background: #1c1c1c;
|
||||||
|
padding: 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 100px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#ff3b30 calc(var(--fill, 100%) * 1%),
|
||||||
|
rgba(255, 255, 255, 0.3) calc(var(--fill, 100%) * 1%)
|
||||||
|
);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -4px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-slider {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.state {
|
|
||||||
padding: 20px;
|
@media (max-width: 768px) {
|
||||||
text-align: center;
|
.rabbit-page {
|
||||||
color: #666;
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: calc(100% + 36px);
|
||||||
|
margin: -16px -18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 96vw;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.state.error {
|
|
||||||
color: #b00020;
|
@media (max-width: 480px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-close-btn {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
const countStore = writable(0);
|
const countStore = writable(0);
|
||||||
export const rabbitCount = countStore;
|
export const rabbitCount = countStore;
|
||||||
@@ -6,3 +7,19 @@ export const rabbitCount = countStore;
|
|||||||
export function setRabbitCount(count) {
|
export function setRabbitCount(count) {
|
||||||
countStore.set(Number(count) || 0);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
125
server/server.js
125
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() {
|
function countRabbitItems() {
|
||||||
try {
|
try {
|
||||||
const entries = fs
|
const entries = fs
|
||||||
@@ -4528,6 +4620,14 @@ function detectMediaFlagsForPath(info, relWithinRoot, isDirectory) {
|
|||||||
const flags = { movies: false, tv: false };
|
const flags = { movies: false, tv: false };
|
||||||
if (!info || typeof info !== "object") return flags;
|
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 normalized = normalizeTrashPath(relWithinRoot);
|
||||||
const matchesPath = (candidate) => {
|
const matchesPath = (candidate) => {
|
||||||
const normalizedCandidate = normalizeTrashPath(candidate);
|
const normalizedCandidate = normalizeTrashPath(candidate);
|
||||||
@@ -4541,8 +4641,7 @@ function detectMediaFlagsForPath(info, relWithinRoot, isDirectory) {
|
|||||||
return normalizedCandidate === normalized;
|
return normalizedCandidate === normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
const files = info.files || {};
|
for (const [key, meta] of Object.entries(infoFiles)) {
|
||||||
for (const [key, meta] of Object.entries(files)) {
|
|
||||||
if (!meta) continue;
|
if (!meta) continue;
|
||||||
if (!matchesPath(key)) continue;
|
if (!matchesPath(key)) continue;
|
||||||
if (meta.movieMatch) flags.movies = true;
|
if (meta.movieMatch) flags.movies = true;
|
||||||
@@ -5518,6 +5617,10 @@ app.delete("/api/file", requireAuth, (req, res) => {
|
|||||||
trashStateCache.delete(folderId);
|
trashStateCache.delete(folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (folderId && removeRabbitMetadata(folderId)) {
|
||||||
|
broadcastRabbitCount();
|
||||||
|
}
|
||||||
|
|
||||||
if (folderId) {
|
if (folderId) {
|
||||||
let matchedInfoHash = null;
|
let matchedInfoHash = null;
|
||||||
for (const [infoHash, entry] of torrents.entries()) {
|
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) ---
|
// --- 🗑️ Çö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 {
|
try {
|
||||||
const { trashName } = req.body;
|
const { trashName } = req.body;
|
||||||
|
|
||||||
@@ -6021,6 +6124,9 @@ app.post("/api/trash/restore", requireAuth, (req, res) => {
|
|||||||
console.log(`♻️ Öğe geri yüklendi: ${safeName}`);
|
console.log(`♻️ Öğe geri yüklendi: ${safeName}`);
|
||||||
|
|
||||||
broadcastFileUpdate(rootFolder);
|
broadcastFileUpdate(rootFolder);
|
||||||
|
if (await rebuildRabbitMetadataForFolder(rootFolder)) {
|
||||||
|
broadcastRabbitCount();
|
||||||
|
}
|
||||||
if (mediaFlags.movies || mediaFlags.tv) {
|
if (mediaFlags.movies || mediaFlags.tv) {
|
||||||
queueMediaRescan({
|
queueMediaRescan({
|
||||||
movies: mediaFlags.movies,
|
movies: mediaFlags.movies,
|
||||||
@@ -6463,9 +6569,22 @@ app.get("/api/rabbit", requireAuth, (req, res) => {
|
|||||||
const items = [];
|
const items = [];
|
||||||
for (const folder of entries) {
|
for (const folder of entries) {
|
||||||
const metaPath = path.join(RABBIT_DATA_ROOT, folder, "metadata.json");
|
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;
|
if (!fs.existsSync(metaPath)) continue;
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
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({
|
items.push({
|
||||||
id: data.id || folder,
|
id: data.id || folder,
|
||||||
title: data.title || folder,
|
title: data.title || folder,
|
||||||
|
|||||||
Reference in New Issue
Block a user