diff --git a/.env.example b/.env.example index c134133..02f69d4 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,33 @@ +# Varsayılan giriş kullanıcı adı; ilk açılışta otomatik kullanıcı oluşturmak için kullanılır. +# Gerçek ortamda tahmin edilmesi zor bir değer seçmeniz önerilir. USERNAME=madafaka +# Varsayılan giriş parolası; ilk kullanıcı oluşturulduktan sonra değiştirilmesi önerilir. +# Güvenlik için güçlü ve benzersiz bir parola kullanın. PASSWORD=superpassword +# JWT erişim tokeni geçerlilik süresi; örn: 15m, 1h gibi değerler alır. +# Çok uzun tutulursa güvenlik riski artar, çok kısa tutulursa kullanıcı oturumu sık yenilenir. JWT_TTL=15m +# Frontend'in backend API adresi; farklı makinelerde çalıştırıyorsanız doğru host/port girin. +# Boş bırakılırsa tarayıcı mevcut origin'i kullanır. VITE_API=http://localhost:3001 +# TMDB API anahtarı; film metadata (poster, başlık, özet vb.) çekmek için gereklidir. +# Boşsa film eşleştirme ve zenginleştirme işlemleri çalışmaz. TMDB_API_KEY="..." +# TVDB API anahtarı; dizi/episode metadata eşleştirmesi için gereklidir. +# Boşsa dizi verileri ve bölüm detayları oluşturulmaz. TVDB_API_KEY="..." +# Video thumbnail almak için kullanılacak zaman noktası; ffmpeg -ss parametresine gider. +# Örn: 10 (saniye) veya 00:00:05 biçiminde ayarlanabilir. VIDEO_THUMBNAIL_TIME=10 -FANART_TV_API_KEY=".." \ No newline at end of file +# Fanart.tv API anahtarı; ekstra görseller/arka planlar için kullanılır. +# Boşsa fanart görselleri yüklenmez. +FANART_TV_API_KEY=".." +# Debug amaçlı CPU kullanımını periyodik olarak loglar; yalnızca teşhis için açın, +# üretim ortamında açık bırakmanız log gürültüsü oluşturur. +DEBUG_CPU=0 +# Torrent tamamlandığında otomatik pause eder; seeding ve arka plan ağ trafiği azalır, +# CPU tüketimini düşürmeye yardımcı olur. Manuel devam ettirmek istersen kapatın. +AUTO_PAUSE_ON_COMPLETE=0 +# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır; +# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır. +DISABLE_MEDIA_PROCESSING=0 diff --git a/Readme.md b/Readme.md index 5841d24..b10a07e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,15 +1,15 @@

- du.pe logo + du.pe logo

# du.pe - Simple, Fast & Lightweight Torrent Server ⚡📦 A **self-hosted torrent-based file manager and media player**, similar to Put.io — fast, minimal, and elegant. -Add torrents, monitor downloads, and instantly stream videos through a clean web interface! 🖥️🎬 +Add torrents, monitor downloads, and instantly stream videos through a clean awesome web interface! 🖥️🎬 --- -## ✨ Features +## ✨ Feature - 🧲 **Add Torrents** - Upload `.torrent` files (via form) diff --git a/client/src/App.svelte b/client/src/App.svelte index d7621be..1992d9b 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import Sidebar from "./components/Sidebar.svelte"; import Topbar from "./components/Topbar.svelte"; + import MiniPlayer from "./components/MiniPlayer.svelte"; import Files from "./routes/Files.svelte"; import Transfers from "./routes/Transfers.svelte"; import Trash from "./routes/Trash.svelte"; @@ -156,6 +157,8 @@ + + {#if menuOpen}
+ import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js"; + import { API, withToken } from "../utils/api.js"; + import { cleanFileName } from "../utils/filename.js"; + + let videoEl = null; + + function thumbnailURL(item) { + if (!item?.thumbnail) return null; + const token = localStorage.getItem("token"); + const separator = item.thumbnail.includes("?") ? "&" : "?"; + return `${API}${item.thumbnail}${separator}token=${token}`; + } + + function formatTime(seconds) { + if (!Number.isFinite(seconds) || seconds < 0) return "0:00"; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${String(secs).padStart(2, "0")}`; + } + + function remainingTime(current, total) { + if (!Number.isFinite(total) || total <= 0) return "-0:00"; + const remaining = Math.max(total - (current || 0), 0); + return `-${formatTime(remaining)}`; + } + + function sourceLabel(item) { + if (item?.tracker === "youtube" || item?.thumbnail) return "YouTube"; + return "Music"; + } + + function videoStreamURL(item) { + if (!item?.infoHash) return null; + const index = item.fileIndex || 0; + return withToken(`${API}/stream/${item.infoHash}?index=${index}`); + } + + $: if (videoEl && $musicPlayer.currentTrack && $musicPlayer.isPlaying) { + const target = $musicPlayer.currentTime || 0; + if (Number.isFinite(target) && Math.abs(videoEl.currentTime - target) > 0.6) { + videoEl.currentTime = target; + } + } + + $: if (videoEl && $musicPlayer.isPlaying) { + videoEl.play().catch(() => undefined); + } else if (videoEl) { + videoEl.pause(); + } + + +{#if $musicPlayer.currentTrack} +
+
+
+
+ {cleanFileName($musicPlayer.currentTrack.title)} +
+
{sourceLabel($musicPlayer.currentTrack)}
+
+ +
+ + +
+
+ +
+ {#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)} + + {:else if thumbnailURL($musicPlayer.currentTrack)} + {$musicPlayer.currentTrack.title} + {:else} +
+ +
+ {/if} +
+ +
+ {formatTime($musicPlayer.currentTime)} +
+
+
+ + {remainingTime($musicPlayer.currentTime, $musicPlayer.duration)} + +
+ +
+ + + + +
+
+{/if} + + diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index 38ee8b6..d2f1469 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -22,17 +22,16 @@ let hasMusic = false; // Store subscription'ı temizlemek için let unsubscribeDiskSpace; + let diskSpaceWs; // Store'u değişkene bağla unsubscribeDiskSpace = diskSpaceStore.subscribe(value => { diskSpace = value; - console.log('🔄 Disk space updated from store:', diskSpace); }); // Disk space'i reaktif olarak güncellemek için bir fonksiyon function updateDiskSpace(newData) { diskSpaceStore.update(current => Object.assign({}, current, newData)); - console.log('🔄 Disk space update called with:', newData); } const unsubscribeMovie = movieCount.subscribe((count) => { @@ -59,6 +58,9 @@ const unsubscribeMusic = musicCount.subscribe((count) => { if (unsubscribeDiskSpace) { unsubscribeDiskSpace(); } + if (diskSpaceWs && (diskSpaceWs.readyState === WebSocket.OPEN || diskSpaceWs.readyState === WebSocket.CONNECTING)) { + diskSpaceWs.close(); + } }); // Menü öğesine tıklanınca sidebar'ı kapat @@ -72,7 +74,6 @@ const unsubscribeMusic = musicCount.subscribe((count) => { const response = await apiFetch('/api/disk-space'); if (response.ok) { const data = await response.json(); - console.log('Disk space data received:', data); updateDiskSpace(data); } else { console.error('Disk space API error:', response.status, response.statusText); @@ -84,7 +85,6 @@ const unsubscribeMusic = musicCount.subscribe((count) => { // Component yüklendiğinde disk space bilgilerini al onMount(() => { - console.log('🔌 Sidebar component mounted'); fetchDiskSpace(); // WebSocket bağlantısı kur @@ -94,41 +94,22 @@ const unsubscribeMusic = musicCount.subscribe((count) => { // Eğer client farklı portta çalışıyorsa, server port'unu manuel belirt const wsHost = currentHost.includes(':3000') ? currentHost.replace(':3000', ':3001') : currentHost; const wsUrl = `${wsProtocol}//${wsHost}`; - console.log('🔌 Connecting to WebSocket at:', wsUrl); + diskSpaceWs = new WebSocket(wsUrl); - // WebSocket bağlantısını global olarak saklayalım - window.diskSpaceWs = new WebSocket(wsUrl); - - window.diskSpaceWs.onmessage = (event) => { + diskSpaceWs.onmessage = (event) => { try { const data = JSON.parse(event.data); - console.log('WebSocket message received:', data); if (data.type === 'diskSpace') { - console.log('Disk space update received:', data.data); updateDiskSpace(data.data); } } catch (err) { console.error('WebSocket message parse error:', err); } }; - - window.diskSpaceWs.onopen = () => { - console.log('WebSocket connected for disk space updates'); - }; - - window.diskSpaceWs.onerror = (error) => { + + diskSpaceWs.onerror = (error) => { console.error('WebSocket error:', error); }; - - window.diskSpaceWs.onclose = () => { - console.log('WebSocket disconnected'); - }; - - onDestroy(() => { - if (window.diskSpaceWs && (window.diskSpaceWs.readyState === WebSocket.OPEN || window.diskSpaceWs.readyState === WebSocket.CONNECTING)) { - window.diskSpaceWs.close(); - } - }); }); diff --git a/client/src/routes/Music.svelte b/client/src/routes/Music.svelte index d38591b..dadcb7a 100644 --- a/client/src/routes/Music.svelte +++ b/client/src/routes/Music.svelte @@ -3,25 +3,40 @@ import { API, apiFetch, withToken } from "../utils/api.js"; import { musicCount } from "../stores/musicStore.js"; import { cleanFileName } from "../utils/filename.js"; + import { + musicPlayer, + setQueue, + playTrack as playFromStore, + togglePlay, + playNext, + playPrevious, + seekToPercent, + setVolume, + toggleMute, + stopPlayback + } from "../stores/musicPlayerStore.js"; let items = []; let loading = true; let error = null; - // Player state - let currentTrack = null; - let isPlaying = false; - let currentTime = 0; - let duration = 0; - let volume = 0.8; - let isMuted = false; - let previousVolume = 0.8; - let audioEl; - let progressInterval; - // View mode + const VIEW_MODE_STORAGE_KEY = "musicViewMode"; let viewMode = "list"; // "list" or "grid" + function loadViewMode() { + if (typeof localStorage === "undefined") return; + const saved = localStorage.getItem(VIEW_MODE_STORAGE_KEY); + if (saved === "list" || saved === "grid") { + viewMode = saved; + } + } + + function persistViewMode(mode) { + if (typeof localStorage === "undefined") return; + localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode); + } + async function loadMusic() { loading = true; error = null; @@ -34,6 +49,7 @@ const list = await resp.json(); items = Array.isArray(list) ? list : []; musicCount.set(items.length); + setQueue(items); } catch (err) { console.error("Music load error:", err); items = []; @@ -77,125 +93,32 @@ // Player functions function playTrack(item, index) { - if (currentTrack?.id === item.id) { + if (!item) return; + const current = $musicPlayer.currentTrack; + if (current?.id === item.id) { togglePlay(); return; } - - currentTrack = { ...item, index }; - currentTime = 0; - duration = item.mediaInfo?.format?.duration || 0; - isPlaying = true; - - if (audioEl) { - audioEl.src = streamURL(item); - audioEl.play().catch((err) => { - console.error("Play error:", err); - isPlaying = false; - }); - } - } - - function togglePlay() { - if (!audioEl) return; - if (isPlaying) { - audioEl.pause(); - } else { - audioEl.play().catch((err) => { - console.error("Play error:", err); - }); - } - isPlaying = !isPlaying; - } - - function playNext() { - if (!currentTrack || items.length === 0) return; - const currentIndex = items.findIndex((i) => i.id === currentTrack.id); - const nextIndex = (currentIndex + 1) % items.length; - playTrack(items[nextIndex], nextIndex); - } - - function playPrevious() { - if (!currentTrack || items.length === 0) return; - const currentIndex = items.findIndex((i) => i.id === currentTrack.id); - const prevIndex = (currentIndex - 1 + items.length) % items.length; - playTrack(items[prevIndex], prevIndex); + playFromStore(item, index, items); } function seek(e) { - if (!audioEl || !duration) return; const percent = parseFloat(e.target.value); - currentTime = (percent / 100) * duration; - audioEl.currentTime = currentTime; + seekToPercent(percent); } - function setVolume(e) { - volume = parseFloat(e.target.value); - if (audioEl) { - audioEl.volume = volume; - } - isMuted = volume === 0; + function setPlayerVolume(e) { + setVolume(e.target.value); } - function toggleMute() { - if (isMuted) { - volume = previousVolume; - isMuted = false; - } else { - previousVolume = volume; - volume = 0; - isMuted = true; - } - if (audioEl) { - audioEl.volume = volume; - } - } - - function handleTimeUpdate() { - if (audioEl) { - currentTime = audioEl.currentTime; - duration = audioEl.duration || 0; - } - } - - function handleLoadedMetadata() { - if (audioEl) { - duration = audioEl.duration || 0; - } - } - - function handleEnded() { - playNext(); - } - - function handlePlay() { - isPlaying = true; - } - - function handlePause() { - isPlaying = false; - } - - function startProgressInterval() { - if (progressInterval) clearInterval(progressInterval); - progressInterval = setInterval(() => { - if (isPlaying && audioEl) { - currentTime = audioEl.currentTime; - } - }, 100); + function setViewMode(mode) { + viewMode = mode; + persistViewMode(mode); } onMount(() => { + loadViewMode(); loadMusic(); - startProgressInterval(); - - return () => { - if (progressInterval) clearInterval(progressInterval); - if (audioEl) { - audioEl.pause(); - audioEl.src = ""; - } - }; }); @@ -212,14 +135,14 @@
- {:else if viewMode === 'list'} + {:else if viewMode === "list"}
@@ -261,14 +184,14 @@
{#each items as item, idx (item.id)}
playTrack(item, idx)} on:dblclick={() => { - if (currentTrack?.id === item.id) togglePlay(); + if ($musicPlayer.currentTrack?.id === item.id) togglePlay(); }} >
- {#if currentTrack?.id === item.id && isPlaying} + {#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
@@ -292,7 +215,9 @@
{sourceLabel(item)}
- {formatDuration(item.mediaInfo?.format?.duration || item.duration)} + {formatDuration( + item.mediaInfo?.format?.duration || item.duration, + )}
- {#if currentTrack?.id === item.id && isPlaying} + {#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
@@ -344,7 +269,9 @@
{cleanFileName(item.title)}
{sourceLabel(item)}
- {formatDuration(item.mediaInfo?.format?.duration || item.duration)} + {formatDuration( + item.mediaInfo?.format?.duration || item.duration, + )}
@@ -354,23 +281,17 @@
- {#if currentTrack} + {#if $musicPlayer.currentTrack}
- -
- {#if thumbnailURL(currentTrack)} - {currentTrack.title} + {#if thumbnailURL($musicPlayer.currentTrack)} + {$musicPlayer.currentTrack.title} {:else}
@@ -378,11 +299,15 @@ {/if}
-
{cleanFileName(currentTrack.title)}
-
{sourceLabel(currentTrack)}
+
+ {cleanFileName($musicPlayer.currentTrack.title)} +
+
+ {sourceLabel($musicPlayer.currentTrack)} +
-
@@ -394,9 +319,9 @@
@@ -618,8 +551,12 @@ } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } /* === List View === */ @@ -690,13 +627,27 @@ animation: equalizer 0.8s ease-in-out infinite; } - .playing-indicator .bar-1 { animation-delay: 0s; height: 8px; } - .playing-indicator .bar-2 { animation-delay: 0.2s; height: 12px; } - .playing-indicator .bar-3 { animation-delay: 0.4s; height: 6px; } + .playing-indicator .bar-1 { + animation-delay: 0s; + height: 8px; + } + .playing-indicator .bar-2 { + animation-delay: 0.2s; + height: 12px; + } + .playing-indicator .bar-3 { + animation-delay: 0.4s; + height: 6px; + } @keyframes equalizer { - 0%, 100% { transform: scaleY(0.5); } - 50% { transform: scaleY(1); } + 0%, + 100% { + transform: scaleY(0.5); + } + 50% { + transform: scaleY(1); + } } .row-thumb { @@ -879,8 +830,13 @@ } @keyframes pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 179, 51, 0.4); } - 50% { box-shadow: 0 0 0 8px rgba(245, 179, 51, 0); } + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(245, 179, 51, 0.4); + } + 50% { + box-shadow: 0 0 0 8px rgba(245, 179, 51, 0); + } } .card-info { @@ -1131,7 +1087,11 @@ height: 4px; border-radius: 2px; cursor: pointer; - background: linear-gradient(to right, var(--yellow) var(--fill, 100%), #e5e5e5 var(--fill, 100%)); + background: linear-gradient( + to right, + var(--yellow) var(--fill, 100%), + #e5e5e5 var(--fill, 100%) + ); position: relative; } diff --git a/client/src/routes/TvShows.svelte b/client/src/routes/TvShows.svelte index e7f836c..c7ec6c4 100644 --- a/client/src/routes/TvShows.svelte +++ b/client/src/routes/TvShows.svelte @@ -1184,7 +1184,7 @@ async function openVideoAtIndex(index) { .tv-overlay-content { position: relative; width: min(1040px, 94vw); - max-height: 90vh; + max-height: 95vh; border-radius: 20px; overflow: hidden; background: rgba(12, 12, 12, 0.5); @@ -1222,7 +1222,7 @@ async function openVideoAtIndex(index) { } .detail-poster { - flex: 0 0 230px; + flex: 0 0 183px; } .detail-poster-img { @@ -1363,7 +1363,7 @@ async function openVideoAtIndex(index) { display: flex; flex-direction: column; gap: 14px; - max-height: 260px; + max-height: 520px; overflow-y: auto; padding-right: 6px; padding-left: 2px; @@ -1691,11 +1691,11 @@ async function openVideoAtIndex(index) { } .detail-poster { - flex: 0 0 200px; + flex: 0 0 72px; } .episode-list { - max-height: 240px; + max-height: 440px; } } @@ -1716,7 +1716,7 @@ async function openVideoAtIndex(index) { } .detail-poster { - flex: 0 0 160px; + flex: 0 0 58px; } .detail-title { @@ -1732,7 +1732,7 @@ async function openVideoAtIndex(index) { } .episode-list { - max-height: 200px; + max-height: 360px; gap: 12px; } @@ -1789,7 +1789,7 @@ async function openVideoAtIndex(index) { } .detail-poster { - flex: 0 0 120px; + flex: 0 0 43px; } .detail-title { @@ -1817,7 +1817,7 @@ async function openVideoAtIndex(index) { } .episode-list { - max-height: 180px; + max-height: 320px; gap: 10px; } diff --git a/client/src/stores/musicPlayerStore.js b/client/src/stores/musicPlayerStore.js new file mode 100644 index 0000000..b727642 --- /dev/null +++ b/client/src/stores/musicPlayerStore.js @@ -0,0 +1,203 @@ +import { writable, get } from "svelte/store"; +import { API, withToken } from "../utils/api.js"; + +const INITIAL_STATE = { + currentTrack: null, + currentIndex: -1, + queue: [], + isPlaying: false, + currentTime: 0, + duration: 0, + volume: 0.8, + isMuted: false +}; + +const { subscribe, set, update } = writable({ ...INITIAL_STATE }); + +let audio = null; +let previousVolume = INITIAL_STATE.volume; + +function ensureAudio() { + if (audio || typeof Audio === "undefined") return; + audio = new Audio(); + audio.preload = "metadata"; + audio.volume = INITIAL_STATE.volume; + + audio.addEventListener("timeupdate", () => { + update((state) => ({ + ...state, + currentTime: audio.currentTime || 0, + duration: Number.isFinite(audio.duration) ? audio.duration : state.duration + })); + }); + + audio.addEventListener("loadedmetadata", () => { + update((state) => ({ + ...state, + duration: Number.isFinite(audio.duration) ? audio.duration : 0 + })); + }); + + audio.addEventListener("play", () => { + update((state) => ({ ...state, isPlaying: true })); + }); + + audio.addEventListener("pause", () => { + update((state) => ({ ...state, isPlaying: false })); + }); + + audio.addEventListener("ended", () => { + playNext(); + }); +} + +function buildStreamURL(item) { + const base = `${API}/stream/${item.infoHash}?index=${item.fileIndex || 0}`; + return withToken(base); +} + +function setQueue(items = []) { + update((state) => ({ ...state, queue: Array.isArray(items) ? items : [] })); +} + +function playTrack(item, index, items = null) { + if (!item) return; + ensureAudio(); + if (items) setQueue(items); + const state = get({ subscribe }); + const nextIndex = + Number.isFinite(index) && index >= 0 + ? index + : state.queue.findIndex((entry) => entry.id === item.id); + + update((prev) => ({ + ...prev, + currentTrack: item, + currentIndex: nextIndex, + currentTime: 0, + duration: item.mediaInfo?.format?.duration || 0 + })); + + if (!audio) return; + audio.src = buildStreamURL(item); + audio + .play() + .catch(() => update((prev) => ({ ...prev, isPlaying: false }))); +} + +function playByIndex(index) { + ensureAudio(); + const state = get({ subscribe }); + if (!state.queue.length || index < 0 || index >= state.queue.length) return; + const item = state.queue[index]; + update((prev) => ({ + ...prev, + currentTrack: item, + currentIndex: index, + currentTime: 0, + duration: item.mediaInfo?.format?.duration || 0 + })); + if (!audio) return; + audio.src = buildStreamURL(item); + audio + .play() + .catch(() => update((prev) => ({ ...prev, isPlaying: false }))); +} + +function togglePlay() { + ensureAudio(); + const state = get({ subscribe }); + if (!audio || !state.currentTrack) return; + if (state.isPlaying) { + audio.pause(); + } else { + audio + .play() + .catch(() => update((prev) => ({ ...prev, isPlaying: false }))); + } +} + +function playNext() { + const state = get({ subscribe }); + if (!state.queue.length) return; + const nextIndex = + state.currentIndex >= 0 + ? (state.currentIndex + 1) % state.queue.length + : 0; + playByIndex(nextIndex); +} + +function playPrevious() { + const state = get({ subscribe }); + if (!state.queue.length) return; + const prevIndex = + state.currentIndex > 0 + ? state.currentIndex - 1 + : state.queue.length - 1; + playByIndex(prevIndex); +} + +function seekToPercent(percent) { + ensureAudio(); + if (!audio) return; + const state = get({ subscribe }); + if (!state.duration) return; + const nextTime = (Number(percent) / 100) * state.duration; + audio.currentTime = nextTime; + update((prev) => ({ ...prev, currentTime: nextTime })); +} + +function setVolume(value) { + ensureAudio(); + const volume = Math.min(Math.max(Number(value) || 0, 0), 1); + if (audio) audio.volume = volume; + update((prev) => ({ + ...prev, + volume, + isMuted: volume === 0 + })); +} + +function toggleMute() { + ensureAudio(); + const state = get({ subscribe }); + if (!audio) return; + if (state.isMuted || state.volume === 0) { + const nextVolume = previousVolume || 0.8; + audio.volume = nextVolume; + update((prev) => ({ + ...prev, + volume: nextVolume, + isMuted: false + })); + } else { + previousVolume = state.volume; + audio.volume = 0; + update((prev) => ({ + ...prev, + volume: 0, + isMuted: true + })); + } +} + +function stopPlayback() { + if (audio) { + audio.pause(); + audio.src = ""; + } + set({ ...INITIAL_STATE }); +} + +export const musicPlayer = { subscribe }; +export { + setQueue, + playTrack, + togglePlay, + playNext, + playPrevious, + seekToPercent, + setVolume, + toggleMute, + stopPlayback +}; diff --git a/docker-compose.yml b/docker-compose.yml index 833c48f..c549a27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,7 @@ -version: "3.9" - services: dupe: build: . - container_name: app + container_name: dupe ports: - "3005:3001" volumes: @@ -18,3 +16,6 @@ services: TVDB_API_KEY: ${TVDB_API_KEY} FANART_TV_API_KEY: ${FANART_TV_API_KEY} VIDEO_THUMBNAIL_TIME: ${VIDEO_THUMBNAIL_TIME} + DEBUG_CPU: ${DEBUG_CPU} + AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE} + DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING} diff --git a/server/server.js b/server/server.js index cfb3084..a37bb75 100644 --- a/server/server.js +++ b/server/server.js @@ -24,6 +24,9 @@ const torrents = new Map(); const youtubeJobs = new Map(); let wss; const PORT = process.env.PORT || 3001; +const DEBUG_CPU = process.env.DEBUG_CPU === "1"; +const DISABLE_MEDIA_PROCESSING = process.env.DISABLE_MEDIA_PROCESSING === "1"; +const AUTO_PAUSE_ON_COMPLETE = process.env.AUTO_PAUSE_ON_COMPLETE === "1"; // --- İndirilen dosyalar için klasör oluştur --- const DOWNLOAD_DIR = path.join(__dirname, "downloads"); @@ -108,10 +111,40 @@ const FFPROBE_MAX_BUFFER = : 10 * 1024 * 1024; const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png"); +function getWsClientCount() { + if (!wss) return 0; + let count = 0; + wss.clients.forEach((c) => { + if (c.readyState === 1) count += 1; + }); + return count; +} + +function startCpuProfiler() { + if (!DEBUG_CPU) return; + const intervalMs = 5000; + let lastUsage = process.cpuUsage(); + let lastTime = process.hrtime.bigint(); + setInterval(() => { + const usage = process.cpuUsage(); + const now = process.hrtime.bigint(); + const deltaUser = usage.user - lastUsage.user; + const deltaSystem = usage.system - lastUsage.system; + const elapsedUs = Number(now - lastTime) / 1000; + const cpuPct = elapsedUs > 0 ? ((deltaUser + deltaSystem) / elapsedUs) * 100 : 0; + lastUsage = usage; + lastTime = now; + console.log( + `📈 CPU ${(cpuPct || 0).toFixed(1)}% | torrents:${torrents.size} yt:${youtubeJobs.size} ws:${getWsClientCount()}` + ); + }, intervalMs); +} + app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use("/downloads", express.static(DOWNLOAD_DIR)); +startCpuProfiler(); // --- En uygun video dosyasını seç --- function pickBestVideoFile(torrent) { @@ -1368,6 +1401,7 @@ function bytesFromHuman(value, unit = "B") { } async function extractMediaInfo(filePath, retryCount = 0) { + if (DISABLE_MEDIA_PROCESSING) return null; if (!filePath || !fs.existsSync(filePath)) return null; // Farklı ffprobe stratejileri @@ -1486,6 +1520,7 @@ async function extractMediaInfo(filePath, retryCount = 0) { } function queueVideoThumbnail(fullPath, relPath, retryCount = 0) { + if (DISABLE_MEDIA_PROCESSING) return; const { relThumb, absThumb } = getVideoThumbnailPaths(relPath); if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return; @@ -1544,6 +1579,7 @@ function queueVideoThumbnail(fullPath, relPath, retryCount = 0) { } function queueImageThumbnail(fullPath, relPath, retryCount = 0) { + if (DISABLE_MEDIA_PROCESSING) return; const { relThumb, absThumb } = getImageThumbnailPaths(relPath); if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return; @@ -2398,6 +2434,14 @@ async function ensureMovieData( bestVideoPath, precomputedMediaInfo = null ) { + if (DISABLE_MEDIA_PROCESSING) { + return { + mediaInfo: precomputedMediaInfo || null, + metadata: null, + cacheKey: null, + videoPath: bestVideoPath ? normalizeTrashPath(bestVideoPath) : null + }; + } const normalizedRoot = sanitizeRelative(rootFolder); if (!TMDB_API_KEY) { return { @@ -2910,6 +2954,7 @@ async function ensureSeriesData( seriesInfo, mediaInfo ) { + if (DISABLE_MEDIA_PROCESSING) return null; if (!TVDB_API_KEY || !seriesInfo) { console.log("📺 TVDB atlandı (eksik anahtar ya da seriesInfo yok):", { rootFolder, @@ -2953,6 +2998,22 @@ async function ensureSeriesData( } } + if (!seriesData && candidateKeys.length) { + for (const key of candidateKeys) { + const candidatePaths = tvSeriesPathsByKey(key); + if (!fs.existsSync(candidatePaths.metadata)) continue; + try { + seriesData = JSON.parse(fs.readFileSync(candidatePaths.metadata, "utf-8")) || {}; + existingPaths = candidatePaths; + break; + } catch (err) { + console.warn( + `⚠️ series.json okunamadı (${candidatePaths.metadata}): ${err.message}` + ); + } + } + } + const legacyPaths = tvSeriesPaths(normalizedRoot); if (!seriesData && fs.existsSync(legacyPaths.metadata)) { try { @@ -4211,6 +4272,7 @@ let pendingMediaRescan = { movies: false, tv: false }; let lastMediaRescanReason = "manual"; function queueMediaRescan({ movies = false, tv = false, reason = "manual" } = {}) { + if (DISABLE_MEDIA_PROCESSING) return; if (!movies && !tv) return; pendingMediaRescan.movies = pendingMediaRescan.movies || movies; pendingMediaRescan.tv = pendingMediaRescan.tv || tv; @@ -4578,7 +4640,7 @@ async function onTorrentDone({ torrent }) { }; const seriesInfo = parseSeriesInfo(file.name); - if (seriesInfo) { + if (seriesInfo && !DISABLE_MEDIA_PROCESSING) { try { const ensured = await ensureSeriesData( rootFolder, @@ -4665,53 +4727,53 @@ async function onTorrentDone({ torrent }) { infoUpdate.seriesEpisodes = seriesEpisodes; } - const ensuredMedia = await ensureMovieData( - rootFolder, - displayName, - bestVideoPath, - primaryMediaInfo - ); - if (ensuredMedia?.mediaInfo) { - infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo; - if (!infoUpdate.files) infoUpdate.files = perFileMetadata; - if (bestVideoPath) { - const entry = infoUpdate.files[bestVideoPath] || {}; - infoUpdate.files[bestVideoPath] = { - ...entry, - movieMatch: ensuredMedia.metadata - ? { - id: ensuredMedia.metadata.id ?? null, - title: - ensuredMedia.metadata.title || - ensuredMedia.metadata.matched_title || - displayName, - year: ensuredMedia.metadata.release_date - ? Number( - ensuredMedia.metadata.release_date.slice(0, 4) - ) - : ensuredMedia.metadata.matched_year || null, - poster: ensuredMedia.metadata.poster_path || null, - backdrop: ensuredMedia.metadata.backdrop_path || null, - cacheKey: ensuredMedia.cacheKey || null, - matchedAt: Date.now() - } - : entry.movieMatch - }; - const movieType = determineMediaType({ - tracker: torrent.announce?.[0] || null, - movieMatch: ensuredMedia.metadata, - seriesEpisode: seriesEpisodes[bestVideoPath] || null, - categories: null, - relPath: bestVideoPath, - audioOnly: false - }); - perFileMetadata[bestVideoPath] = { - ...(perFileMetadata[bestVideoPath] || {}), - type: movieType - }; - infoUpdate.files[bestVideoPath].type = movieType; + if (!DISABLE_MEDIA_PROCESSING) { + const ensuredMedia = await ensureMovieData( + rootFolder, + displayName, + bestVideoPath, + primaryMediaInfo + ); + if (ensuredMedia?.mediaInfo) { + infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo; + if (!infoUpdate.files) infoUpdate.files = perFileMetadata; + if (bestVideoPath) { + const fileEntry = infoUpdate.files[bestVideoPath] || {}; + infoUpdate.files[bestVideoPath] = { + ...fileEntry, + movieMatch: ensuredMedia.metadata + ? { + id: ensuredMedia.metadata.id ?? null, + title: + ensuredMedia.metadata.title || + ensuredMedia.metadata.matched_title || + displayName, + year: ensuredMedia.metadata.release_date + ? Number(ensuredMedia.metadata.release_date.slice(0, 4)) + : ensuredMedia.metadata.matched_year || null, + poster: ensuredMedia.metadata.poster_path || null, + backdrop: ensuredMedia.metadata.backdrop_path || null, + cacheKey: ensuredMedia.cacheKey || null, + matchedAt: Date.now() + } + : fileEntry.movieMatch + }; + const movieType = determineMediaType({ + tracker: torrent.announce?.[0] || null, + movieMatch: ensuredMedia.metadata, + seriesEpisode: seriesEpisodes[bestVideoPath] || null, + categories: null, + relPath: bestVideoPath, + audioOnly: false + }); + perFileMetadata[bestVideoPath] = { + ...(perFileMetadata[bestVideoPath] || {}), + type: movieType + }; + infoUpdate.files[bestVideoPath].type = movieType; + } + } } -} upsertInfoFile(entry.savePath, infoUpdate); broadcastFileUpdate(rootFolder); @@ -4719,6 +4781,13 @@ async function onTorrentDone({ torrent }) { // Torrent tamamlandığında disk space bilgisini güncelle broadcastDiskSpace(); + if (AUTO_PAUSE_ON_COMPLETE) { + const paused = pauseTorrentEntry(entry); + if (paused) { + console.log(`⏸️ Torrent otomatik durduruldu: ${torrent.infoHash}`); + } + } + // Medya tespiti tamamlandığında özel bildirim gönder if (Object.keys(seriesEpisodes).length > 0 || infoUpdate.primaryMediaInfo) { if (wss) { @@ -5923,6 +5992,10 @@ app.get("/api/movies", requireAuth, (req, res) => { }); async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = false } = {}) { + if (DISABLE_MEDIA_PROCESSING) { + console.log("🎬 Medya işlemleri kapalı; movie metadata taraması atlandı."); + return []; + } if (!TMDB_API_KEY) { throw new Error("TMDB API key tanımlı değil."); } @@ -6746,6 +6819,10 @@ app.get("/api/music", requireAuth, (req, res) => { }); async function rebuildTvMetadata({ clearCache = false } = {}) { + if (DISABLE_MEDIA_PROCESSING) { + console.log("📺 Medya işlemleri kapalı; TV metadata taraması atlandı."); + return []; + } if (!TVDB_API_KEY) { throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil."); }