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 @@
+ 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}
+
+
+
+
+ {#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)}
+
+ {:else if thumbnailURL($musicPlayer.currentTrack)}
+
+ {:else}
+
+
+
+ {/if}
+
+
+
+
{formatTime($musicPlayer.currentTime)}
+
+
+ {remainingTime($musicPlayer.currentTime, $musicPlayer.duration)}
+
+
+
+
+
+
+
+
+ {#if $musicPlayer.isPlaying}
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+{/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 @@
viewMode = 'list'}
+ on:click={() => setViewMode("list")}
title="Liste görünümü"
>
viewMode = 'grid'}
+ on:click={() => setViewMode("grid")}
title="Grid görünümü"
>
@@ -248,7 +171,7 @@
Henüz müzik dosyası yok.
- {:else if viewMode === 'list'}
+ {:else if viewMode === "list"}
{#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,
+ )}
{#each items as item, idx (item.id)}
playTrack(item, idx)}
>
@@ -334,7 +259,7 @@
- {#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)}
-
+ {#if thumbnailURL($musicPlayer.currentTrack)}
+
{:else}
@@ -378,11 +299,15 @@
{/if}
-
{cleanFileName(currentTrack.title)}
-
{sourceLabel(currentTrack)}
+
+ {cleanFileName($musicPlayer.currentTrack.title)}
+
+
+ {sourceLabel($musicPlayer.currentTrack)}
+
-
-
+
+
@@ -394,9 +319,9 @@
- {#if isPlaying}
+ {#if $musicPlayer.isPlaying}
{:else}
@@ -409,29 +334,37 @@
-
{formatTime(currentTime)}
+
+ {formatTime($musicPlayer.currentTime)}
+
-
{formatTime(duration)}
+
{formatTime($musicPlayer.duration)}
@@ -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.");
}