From 92fa8eac4cc3b11bee78813fd9d31cecb7508c22 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 20 Dec 2025 20:18:28 +0000 Subject: [PATCH 01/24] =?UTF-8?q?refactor(deployment):=20konteyner=20port?= =?UTF-8?q?=20maruziyetini=20de=C4=9Fi=C5=9Ftir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uygulamanın harici erişim portunu 3001'den 3005'e güncelledi. Bu değişiklik port çakışmalarını önlemek ve farklı bir port üzerinden hizmete erişim sağlamak amacıyla yapıldı. Konteyner içindeki port (3001) aynı kalırken, ana makine port maruziyeti değiştirildi. --- docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d0a7aa6..018c213 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,9 @@ -version: "3.9" - services: dupe: build: . container_name: app ports: - - "3001:3001" + - "3005:3001" volumes: - ./downloads:/app/server/downloads - ./cache:/app/server/cache From b7adf9ca63e12dd520e7e301f2f6eae1033e4d17 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 20 Dec 2025 20:32:29 +0000 Subject: [PATCH 02/24] =?UTF-8?q?docs:=20readme=20ba=C5=9Fl=C4=B1=C4=9F?= =?UTF-8?q?=C4=B1n=C4=B1=20d=C3=BCzelt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 5841d24..5baad94 100644 --- a/Readme.md +++ b/Readme.md @@ -9,7 +9,7 @@ Add torrents, monitor downloads, and instantly stream videos through a clean web --- -## ✨ Features +## ✨ Feature - 🧲 **Add Torrents** - Upload `.torrent` files (via form) From 8513d7f2dfd36bdc9662da3ad293973e3b3482e7 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 20 Dec 2025 20:43:10 +0000 Subject: [PATCH 03/24] =?UTF-8?q?Logo=20png=20olarak=20g=C3=BCncellendi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 5baad94..a5cc192 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,5 @@

- du.pe logo + du.pe logo

# du.pe - Simple, Fast & Lightweight Torrent Server ⚡📦 From 3078f945a684c509ac80c1972ef72b0abdf85f77 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Tue, 6 Jan 2026 22:58:09 +0300 Subject: [PATCH 04/24] =?UTF-8?q?fix(music):=20audio=20oynat=C4=B1c=C4=B1d?= =?UTF-8?q?a=20dom=20referans=C4=B1n=C4=B1=20d=C3=BCzelt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/routes/Music.svelte | 85 ++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/client/src/routes/Music.svelte b/client/src/routes/Music.svelte index d38591b..dda39f5 100644 --- a/client/src/routes/Music.svelte +++ b/client/src/routes/Music.svelte @@ -1,5 +1,5 @@ diff --git a/server/server.js b/server/server.js index cfb3084..9923d07 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, @@ -4211,6 +4256,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 +4624,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 +4711,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 +4765,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 +5976,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 +6803,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."); } From 6cb415687a766800623290e0be2bb59c1f1369ef Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 10 Jan 2026 13:35:16 +0300 Subject: [PATCH 10/24] =?UTF-8?q?chore(sidebar):=20konsol=20loglar=C4=B1n?= =?UTF-8?q?=C4=B1=20kald=C4=B1r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Sidebar.svelte | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index f21789d..d2f1469 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -27,13 +27,11 @@ let hasMusic = false; // 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) => { @@ -76,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); @@ -88,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 @@ -98,34 +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); 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); } }; - - diskSpaceWs.onopen = () => { - console.log('WebSocket connected for disk space updates'); - }; - + diskSpaceWs.onerror = (error) => { console.error('WebSocket error:', error); }; - - diskSpaceWs.onclose = () => { - console.log('WebSocket disconnected'); - }; }); From c945458a81e96572ab568370c9ad8f85e10a394b Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 11 Jan 2026 14:45:45 +0300 Subject: [PATCH 11/24] =?UTF-8?q?fix(server):=20seri=20verisi=20i=C3=A7in?= =?UTF-8?q?=20aday=20anahtar=20kontrol=C3=BC=20ekle.=20TV=20Shows=20da=20d?= =?UTF-8?q?izi=20b=C3=B6l=C3=BCmlerinin=20tamam=C4=B1n=C4=B1n=20listelenme?= =?UTF-8?q?sini=20sa=C4=9Fla.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureSeriesData fonksiyonuna, veri bulunamadığında candidateKeys listesini kullanarak alternatif dosya yollarının kontrol edilmesi ve ilgili metadatanın yüklenmesi sağlandı. --- client/src/routes/TvShows.svelte | 18 +++++++++--------- server/server.js | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) 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/server/server.js b/server/server.js index 9923d07..a37bb75 100644 --- a/server/server.js +++ b/server/server.js @@ -2998,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 { From d5d91848726eb8c137ef396d8721c6d32f76f743 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 01:51:15 +0300 Subject: [PATCH 12/24] feat(music): mini player ekle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Müzik çalar durumunu yönetmek için global store oluştur. Özel bir mini player bileşeni ile çalma listesi ve kontrolleri ekle. Müzik çaların uygulama genelinde kalıcı olmasını sağla. --- client/src/App.svelte | 3 + client/src/components/MiniPlayer.svelte | 246 ++++++++++++++++++++++++ client/src/routes/Music.svelte | 211 ++++++-------------- client/src/stores/musicPlayerStore.js | 203 +++++++++++++++++++ 4 files changed, 510 insertions(+), 153 deletions(-) create mode 100644 client/src/components/MiniPlayer.svelte create mode 100644 client/src/stores/musicPlayerStore.js 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 } from "../utils/api.js"; + import { cleanFileName } from "../utils/filename.js"; + + 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"; + } + + +{#if $musicPlayer.currentTrack} +
+
+
+
+ {cleanFileName($musicPlayer.currentTrack.title)} +
+
{sourceLabel($musicPlayer.currentTrack)}
+
+ +
+ + +
+
+ +
+ {#if thumbnailURL($musicPlayer.currentTrack)} + {$musicPlayer.currentTrack.title} + {:else} +
+ +
+ {/if} +
+ +
+ {formatTime($musicPlayer.currentTime)} +
+
+
+ + {remainingTime($musicPlayer.currentTime, $musicPlayer.duration)} + +
+ +
+ + + + +
+
+{/if} + + diff --git a/client/src/routes/Music.svelte b/client/src/routes/Music.svelte index 4063516..dadcb7a 100644 --- a/client/src/routes/Music.svelte +++ b/client/src/routes/Music.svelte @@ -1,24 +1,25 @@ @@ -281,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}
@@ -340,7 +243,7 @@
{#each items as item, idx (item.id)}
playTrack(item, idx)} >
@@ -356,7 +259,7 @@
- {#if currentTrack?.id === item.id && isPlaying} + {#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
@@ -378,23 +281,17 @@
- {#if currentTrack} + {#if $musicPlayer.currentTrack}
- -
- {#if thumbnailURL(currentTrack)} - {currentTrack.title} + {#if thumbnailURL($musicPlayer.currentTrack)} + {$musicPlayer.currentTrack.title} {:else}
@@ -402,11 +299,15 @@ {/if}
-
{cleanFileName(currentTrack.title)}
-
{sourceLabel(currentTrack)}
+
+ {cleanFileName($musicPlayer.currentTrack.title)} +
+
+ {sourceLabel($musicPlayer.currentTrack)} +
-
@@ -418,9 +319,9 @@
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 +}; From c9c7686ef1a2c69395fc681f888ba9fa3e4a5b75 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 15:34:14 +0300 Subject: [PATCH 13/24] =?UTF-8?q?fix(readme):=20"clean"=20kelimesini=20"aw?= =?UTF-8?q?esome"=20ile=20g=C3=BCncelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index a5cc192..b10a07e 100644 --- a/Readme.md +++ b/Readme.md @@ -5,7 +5,7 @@ # 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! 🖥️🎬 --- From a722d87f0fda81065f8b9ab3b28783c15ff7d514 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 16:38:00 +0300 Subject: [PATCH 14/24] title change --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a850281..8954bfc 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - du.pe + dupe
From 2eba40c715bc3e93293208a81d1ed672792c146f Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:19:09 +0300 Subject: [PATCH 15/24] =?UTF-8?q?style(client):=20"du.pe"=20ba=C5=9Fl?= =?UTF-8?q?=C4=B1=C4=9F=C4=B1n=C4=B1=20"dupe"=20ile=20g=C3=BCncelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 8954bfc..a850281 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - dupe + du.pe
From 201480cf629704b9267929f5b009cdb310090c87 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:19:56 +0300 Subject: [PATCH 16/24] =?UTF-8?q?style(client):=20ba=C5=9Fl=C4=B1k=20"du.p?= =?UTF-8?q?e"=20yerine=20"dupe"=20olarak=20g=C3=BCncellendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a850281..8954bfc 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - du.pe + dupe
From 424b2f0c7eb106c2b2b2d915aeafd960f50633f8 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:30:23 +0300 Subject: [PATCH 17/24] =?UTF-8?q?UI=20Update:=20Title=20de=C4=9Fi=C5=9Fti?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 8954bfc..a850281 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - dupe + du.pe
From 95f05df4ca2a4085fed864d89f05296e6796752e Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:37:21 +0300 Subject: [PATCH 18/24] UI Change: Title update. --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a850281..a534287 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - du.pe + wisecolt
From e7044ac8c2c06443a701b205efbb00552c954b9a Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:38:21 +0300 Subject: [PATCH 19/24] UI Change: Title update --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a534287..a850281 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - wisecolt + du.pe
From 05b95dec6494b83dfb802a7f038b223e1f2e9483 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:40:13 +0300 Subject: [PATCH 20/24] UI Change: Title update --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a850281..39c3253 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - du.pe + wise
From 1564edc3168ad150f69245f51cf39927ff8df900 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 17:42:01 +0300 Subject: [PATCH 21/24] UI Change: Title update --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 39c3253..a850281 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - wise + du.pe
From 987c6986930586f031083e860fff41efe97935a8 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 18 Jan 2026 18:13:31 +0300 Subject: [PATCH 22/24] Title change --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index a850281..8f984b8 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - du.pe + wise-dupe
From d27a4637b0f9787626b7ae9af484b5482b1fc254 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 19 Jan 2026 17:36:15 +0300 Subject: [PATCH 23/24] =?UTF-8?q?feat(ui):=20sayfa=20ba=C5=9Fl=C4=B1=C4=9F?= =?UTF-8?q?=C4=B1n=C4=B1=20du.pe=20olarak=20g=C3=BCncelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 8f984b8..a850281 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - wise-dupe + du.pe
From 1bad4f7256bc2dd0b4f1f7b726620fc20253a919 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 19 Jan 2026 17:37:06 +0300 Subject: [PATCH 24/24] =?UTF-8?q?feat(ui):=20mini=20oynat=C4=B1c=C4=B1ya?= =?UTF-8?q?=20video=20=C3=B6nizlemesi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parça çalınırken video akışı mevcutsa küçük resim yerine video öğesi gösterilir. Video konumu, oynatma zamanı ile senkronize edilir. --- client/src/components/MiniPlayer.svelte | 39 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/client/src/components/MiniPlayer.svelte b/client/src/components/MiniPlayer.svelte index 5868160..7a0168b 100644 --- a/client/src/components/MiniPlayer.svelte +++ b/client/src/components/MiniPlayer.svelte @@ -1,8 +1,10 @@ {#if $musicPlayer.currentTrack} @@ -50,7 +71,15 @@
- {#if thumbnailURL($musicPlayer.currentTrack)} + {#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)} + + {:else if thumbnailURL($musicPlayer.currentTrack)}