main #3

Merged
wisecolt merged 32 commits from main into develope 2026-01-19 18:11:18 +00:00
10 changed files with 786 additions and 255 deletions

View File

@@ -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 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 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 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 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="..." 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="..." 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 VIDEO_THUMBNAIL_TIME=10
FANART_TV_API_KEY=".." # 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

View File

@@ -1,15 +1,15 @@
<p align="center"> <p align="center">
<img src="https://images2.imgbox.com/61/17/YLrfUgAj_o.jpeg" alt="du.pe logo" width="250" height="250" /> <img src="https://images2.imgbox.com/82/08/VHSBbSxh_o.png" alt="du.pe logo" width="250" height="250" />
</p> </p>
# du.pe - Simple, Fast & Lightweight Torrent Server ⚡📦 # 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. 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** - 🧲 **Add Torrents**
- Upload `.torrent` files (via form) - Upload `.torrent` files (via form)

View File

@@ -3,6 +3,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Sidebar from "./components/Sidebar.svelte"; import Sidebar from "./components/Sidebar.svelte";
import Topbar from "./components/Topbar.svelte"; import Topbar from "./components/Topbar.svelte";
import MiniPlayer from "./components/MiniPlayer.svelte";
import Files from "./routes/Files.svelte"; import Files from "./routes/Files.svelte";
import Transfers from "./routes/Transfers.svelte"; import Transfers from "./routes/Transfers.svelte";
import Trash from "./routes/Trash.svelte"; import Trash from "./routes/Trash.svelte";
@@ -156,6 +157,8 @@
<Route path="/trash" component={Trash} /> <Route path="/trash" component={Trash} />
</div> </div>
<MiniPlayer />
<!-- Sidebar dışına tıklayınca kapanma --> <!-- Sidebar dışına tıklayınca kapanma -->
{#if menuOpen} {#if menuOpen}
<div <div

View File

@@ -0,0 +1,281 @@
<script>
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();
}
</script>
{#if $musicPlayer.currentTrack}
<div class="mini-player" aria-label="Mini music player">
<div class="mini-top">
<div class="mini-user-meta">
<div class="mini-user-name">
{cleanFileName($musicPlayer.currentTrack.title)}
</div>
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
</div>
<div class="mini-actions">
<button class="mini-icon-btn" title="Paylaş">
<i class="fa-solid fa-arrow-up-from-bracket"></i>
</button>
<button class="mini-icon-btn" title="Beğen">
<i class="fa-regular fa-heart"></i>
</button>
</div>
</div>
<div class="mini-cover">
{#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)}
<video
bind:this={videoEl}
src={videoStreamURL($musicPlayer.currentTrack)}
muted
playsinline
preload="metadata"
></video>
{:else if thumbnailURL($musicPlayer.currentTrack)}
<img
src={thumbnailURL($musicPlayer.currentTrack)}
alt={$musicPlayer.currentTrack.title}
/>
{:else}
<div class="mini-cover-placeholder">
<i class="fa-solid fa-music"></i>
</div>
{/if}
</div>
<div class="mini-progress">
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
<div class="mini-bar">
<div
class="mini-bar-fill"
style="width: {($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0}%"
></div>
</div>
<span class="mini-time">
{remainingTime($musicPlayer.currentTime, $musicPlayer.duration)}
</span>
</div>
<div class="mini-controls">
<button class="mini-btn" on:click={playPrevious} title="Önceki">
<i class="fa-solid fa-backward-step"></i>
</button>
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur">
{#if $musicPlayer.isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<button class="mini-btn" on:click={playNext} title="Sonraki">
<i class="fa-solid fa-forward-step"></i>
</button>
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
{/if}
<style>
.mini-player {
position: fixed;
right: 24px;
bottom: 24px;
width: 240px;
padding: 16px;
background: rgba(12, 12, 12, 0.72);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 26px;
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
color: #f6f6f6;
backdrop-filter: blur(14px);
z-index: 50;
}
.mini-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.mini-user-meta {
min-width: 0;
text-align: left;
}
.mini-user-name {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mini-user-handle {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
}
.mini-actions {
display: flex;
gap: 8px;
}
.mini-icon-btn {
width: 32px;
height: 32px;
border-radius: 12px;
border: none;
background: rgba(255, 255, 255, 0.12);
color: #f6f6f6;
cursor: pointer;
}
.mini-cover {
margin: 14px auto 12px;
width: 160px;
height: 160px;
border-radius: 20px;
overflow: hidden;
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
display: grid;
place-items: center;
transform: translateY(-6px);
}
.mini-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.mini-cover video {
width: 100%;
height: 100%;
object-fit: cover;
}
.mini-cover-placeholder {
color: rgba(255, 255, 255, 0.45);
font-size: 40px;
}
.mini-progress {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
margin: 8px 0 14px;
}
.mini-time {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
}
.mini-bar {
height: 3px;
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
overflow: hidden;
}
.mini-bar-fill {
height: 100%;
background: var(--yellow);
border-radius: 999px;
}
.mini-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.mini-btn {
width: 34px;
height: 34px;
border-radius: 999px;
border: none;
background: rgba(255, 255, 255, 0.14);
color: #f6f6f6;
cursor: pointer;
}
.mini-btn.main {
width: 44px;
height: 44px;
border-radius: 999px;
background: var(--yellow);
color: #1e1e1e;
}
.mini-btn.ghost {
background: rgba(255, 255, 255, 0.06);
}
@media (max-width: 820px) {
.mini-player {
right: 16px;
bottom: 16px;
width: 220px;
}
.mini-cover {
width: 140px;
height: 140px;
}
}
</style>

View File

@@ -22,17 +22,16 @@ let hasMusic = false;
// Store subscription'ı temizlemek için // Store subscription'ı temizlemek için
let unsubscribeDiskSpace; let unsubscribeDiskSpace;
let diskSpaceWs;
// Store'u değişkene bağla // Store'u değişkene bağla
unsubscribeDiskSpace = diskSpaceStore.subscribe(value => { unsubscribeDiskSpace = diskSpaceStore.subscribe(value => {
diskSpace = value; diskSpace = value;
console.log('🔄 Disk space updated from store:', diskSpace);
}); });
// Disk space'i reaktif olarak güncellemek için bir fonksiyon // Disk space'i reaktif olarak güncellemek için bir fonksiyon
function updateDiskSpace(newData) { function updateDiskSpace(newData) {
diskSpaceStore.update(current => Object.assign({}, current, newData)); diskSpaceStore.update(current => Object.assign({}, current, newData));
console.log('🔄 Disk space update called with:', newData);
} }
const unsubscribeMovie = movieCount.subscribe((count) => { const unsubscribeMovie = movieCount.subscribe((count) => {
@@ -59,6 +58,9 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
if (unsubscribeDiskSpace) { if (unsubscribeDiskSpace) {
unsubscribeDiskSpace(); unsubscribeDiskSpace();
} }
if (diskSpaceWs && (diskSpaceWs.readyState === WebSocket.OPEN || diskSpaceWs.readyState === WebSocket.CONNECTING)) {
diskSpaceWs.close();
}
}); });
// Menü öğesine tıklanınca sidebar'ı kapat // Menü öğesine tıklanınca sidebar'ı kapat
@@ -72,7 +74,6 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
const response = await apiFetch('/api/disk-space'); const response = await apiFetch('/api/disk-space');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
console.log('Disk space data received:', data);
updateDiskSpace(data); updateDiskSpace(data);
} else { } else {
console.error('Disk space API error:', response.status, response.statusText); 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 // Component yüklendiğinde disk space bilgilerini al
onMount(() => { onMount(() => {
console.log('🔌 Sidebar component mounted');
fetchDiskSpace(); fetchDiskSpace();
// WebSocket bağlantısı kur // 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 // Eğer client farklı portta çalışıyorsa, server port'unu manuel belirt
const wsHost = currentHost.includes(':3000') ? currentHost.replace(':3000', ':3001') : currentHost; const wsHost = currentHost.includes(':3000') ? currentHost.replace(':3000', ':3001') : currentHost;
const wsUrl = `${wsProtocol}//${wsHost}`; const wsUrl = `${wsProtocol}//${wsHost}`;
console.log('🔌 Connecting to WebSocket at:', wsUrl); diskSpaceWs = new WebSocket(wsUrl);
// WebSocket bağlantısını global olarak saklayalım diskSpaceWs.onmessage = (event) => {
window.diskSpaceWs = new WebSocket(wsUrl);
window.diskSpaceWs.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
if (data.type === 'diskSpace') { if (data.type === 'diskSpace') {
console.log('Disk space update received:', data.data);
updateDiskSpace(data.data); updateDiskSpace(data.data);
} }
} catch (err) { } catch (err) {
console.error('WebSocket message parse error:', err); console.error('WebSocket message parse error:', err);
} }
}; };
window.diskSpaceWs.onopen = () => { diskSpaceWs.onerror = (error) => {
console.log('WebSocket connected for disk space updates');
};
window.diskSpaceWs.onerror = (error) => {
console.error('WebSocket error:', 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();
}
});
}); });
</script> </script>

View File

@@ -3,25 +3,40 @@
import { API, apiFetch, withToken } from "../utils/api.js"; import { API, apiFetch, withToken } from "../utils/api.js";
import { musicCount } from "../stores/musicStore.js"; import { musicCount } from "../stores/musicStore.js";
import { cleanFileName } from "../utils/filename.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 items = [];
let loading = true; let loading = true;
let error = null; 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 // View mode
const VIEW_MODE_STORAGE_KEY = "musicViewMode";
let viewMode = "list"; // "list" or "grid" 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() { async function loadMusic() {
loading = true; loading = true;
error = null; error = null;
@@ -34,6 +49,7 @@
const list = await resp.json(); const list = await resp.json();
items = Array.isArray(list) ? list : []; items = Array.isArray(list) ? list : [];
musicCount.set(items.length); musicCount.set(items.length);
setQueue(items);
} catch (err) { } catch (err) {
console.error("Music load error:", err); console.error("Music load error:", err);
items = []; items = [];
@@ -77,125 +93,32 @@
// Player functions // Player functions
function playTrack(item, index) { function playTrack(item, index) {
if (currentTrack?.id === item.id) { if (!item) return;
const current = $musicPlayer.currentTrack;
if (current?.id === item.id) {
togglePlay(); togglePlay();
return; return;
} }
playFromStore(item, index, items);
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);
} }
function seek(e) { function seek(e) {
if (!audioEl || !duration) return;
const percent = parseFloat(e.target.value); const percent = parseFloat(e.target.value);
currentTime = (percent / 100) * duration; seekToPercent(percent);
audioEl.currentTime = currentTime;
} }
function setVolume(e) { function setPlayerVolume(e) {
volume = parseFloat(e.target.value); setVolume(e.target.value);
if (audioEl) {
audioEl.volume = volume;
}
isMuted = volume === 0;
} }
function toggleMute() { function setViewMode(mode) {
if (isMuted) { viewMode = mode;
volume = previousVolume; persistViewMode(mode);
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);
} }
onMount(() => { onMount(() => {
loadViewMode();
loadMusic(); loadMusic();
startProgressInterval();
return () => {
if (progressInterval) clearInterval(progressInterval);
if (audioEl) {
audioEl.pause();
audioEl.src = "";
}
};
}); });
</script> </script>
@@ -212,14 +135,14 @@
<div class="view-toggle"> <div class="view-toggle">
<button <button
class="view-btn {viewMode === 'list' ? 'active' : ''}" class="view-btn {viewMode === 'list' ? 'active' : ''}"
on:click={() => viewMode = 'list'} on:click={() => setViewMode("list")}
title="Liste görünümü" title="Liste görünümü"
> >
<i class="fa-solid fa-list"></i> <i class="fa-solid fa-list"></i>
</button> </button>
<button <button
class="view-btn {viewMode === 'grid' ? 'active' : ''}" class="view-btn {viewMode === 'grid' ? 'active' : ''}"
on:click={() => viewMode = 'grid'} on:click={() => setViewMode("grid")}
title="Grid görünümü" title="Grid görünümü"
> >
<i class="fa-solid fa-border-all"></i> <i class="fa-solid fa-border-all"></i>
@@ -248,7 +171,7 @@
<i class="fa-solid fa-music"></i> <i class="fa-solid fa-music"></i>
<p>Henüz müzik dosyası yok.</p> <p>Henüz müzik dosyası yok.</p>
</div> </div>
{:else if viewMode === 'list'} {:else if viewMode === "list"}
<!-- List View --> <!-- List View -->
<div class="music-list"> <div class="music-list">
<div class="list-header"> <div class="list-header">
@@ -261,14 +184,14 @@
</div> </div>
{#each items as item, idx (item.id)} {#each items as item, idx (item.id)}
<div <div
class="music-row {currentTrack?.id === item.id ? 'playing' : ''}" class="music-row {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)} on:click={() => playTrack(item, idx)}
on:dblclick={() => { on:dblclick={() => {
if (currentTrack?.id === item.id) togglePlay(); if ($musicPlayer.currentTrack?.id === item.id) togglePlay();
}} }}
> >
<div class="row-index"> <div class="row-index">
{#if currentTrack?.id === item.id && isPlaying} {#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
<div class="playing-indicator"> <div class="playing-indicator">
<span class="bar bar-1"></span> <span class="bar bar-1"></span>
<span class="bar bar-2"></span> <span class="bar bar-2"></span>
@@ -292,7 +215,9 @@
<div class="row-source">{sourceLabel(item)}</div> <div class="row-source">{sourceLabel(item)}</div>
</div> </div>
<div class="row-time"> <div class="row-time">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)} {formatDuration(
item.mediaInfo?.format?.duration || item.duration,
)}
</div> </div>
<div class="row-actions" on:click|stopPropagation> <div class="row-actions" on:click|stopPropagation>
<button <button
@@ -318,7 +243,7 @@
<div class="music-grid"> <div class="music-grid">
{#each items as item, idx (item.id)} {#each items as item, idx (item.id)}
<div <div
class="music-card {currentTrack?.id === item.id ? 'playing' : ''}" class="music-card {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)} on:click={() => playTrack(item, idx)}
> >
<div class="card-thumb"> <div class="card-thumb">
@@ -334,7 +259,7 @@
<i class="fa-solid fa-play"></i> <i class="fa-solid fa-play"></i>
</button> </button>
</div> </div>
{#if currentTrack?.id === item.id && isPlaying} {#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
<div class="playing-badge"> <div class="playing-badge">
<i class="fa-solid fa-volume-high"></i> <i class="fa-solid fa-volume-high"></i>
</div> </div>
@@ -344,7 +269,9 @@
<div class="card-title">{cleanFileName(item.title)}</div> <div class="card-title">{cleanFileName(item.title)}</div>
<div class="card-source">{sourceLabel(item)}</div> <div class="card-source">{sourceLabel(item)}</div>
<div class="card-duration"> <div class="card-duration">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)} {formatDuration(
item.mediaInfo?.format?.duration || item.duration,
)}
</div> </div>
</div> </div>
</div> </div>
@@ -354,23 +281,17 @@
</div> </div>
<!-- Bottom Player Bar --> <!-- Bottom Player Bar -->
{#if currentTrack} {#if $musicPlayer.currentTrack}
<div class="player-bar"> <div class="player-bar">
<audio
bind:this={audioEl}
on:timeupdate={handleTimeUpdate}
on:loadedmetadata={handleLoadedMetadata}
on:ended={handleEnded}
on:play={handlePlay}
on:pause={handlePause}
></audio>
<div class="player-content"> <div class="player-content">
<!-- Track Info --> <!-- Track Info -->
<div class="player-track"> <div class="player-track">
<div class="track-thumb"> <div class="track-thumb">
{#if thumbnailURL(currentTrack)} {#if thumbnailURL($musicPlayer.currentTrack)}
<img src={thumbnailURL(currentTrack)} alt={currentTrack.title} /> <img
src={thumbnailURL($musicPlayer.currentTrack)}
alt={$musicPlayer.currentTrack.title}
/>
{:else} {:else}
<div class="thumb-placeholder"> <div class="thumb-placeholder">
<i class="fa-solid fa-music"></i> <i class="fa-solid fa-music"></i>
@@ -378,11 +299,15 @@
{/if} {/if}
</div> </div>
<div class="track-info"> <div class="track-info">
<div class="track-name">{cleanFileName(currentTrack.title)}</div> <div class="track-name">
<div class="track-source">{sourceLabel(currentTrack)}</div> {cleanFileName($musicPlayer.currentTrack.title)}
</div>
<div class="track-source">
{sourceLabel($musicPlayer.currentTrack)}
</div>
</div> </div>
<button class="like-btn" title="Beğen"> <button class="like-btn" title="Kapat" on:click={stopPlayback}>
<i class="fa-regular fa-heart"></i> <i class="fa-solid fa-xmark"></i>
</button> </button>
</div> </div>
@@ -394,9 +319,9 @@
<button <button
class="control-btn play-pause-btn" class="control-btn play-pause-btn"
on:click={togglePlay} on:click={togglePlay}
title={isPlaying ? "Durdaklat" : "Oynat"} title={$musicPlayer.isPlaying ? "Durdaklat" : "Oynat"}
> >
{#if isPlaying} {#if $musicPlayer.isPlaying}
<i class="fa-solid fa-pause"></i> <i class="fa-solid fa-pause"></i>
{:else} {:else}
<i class="fa-solid fa-play"></i> <i class="fa-solid fa-play"></i>
@@ -409,29 +334,37 @@
<!-- Progress --> <!-- Progress -->
<div class="player-progress"> <div class="player-progress">
<span class="time-current">{formatTime(currentTime)}</span> <span class="time-current">
{formatTime($musicPlayer.currentTime)}
</span>
<div class="progress-container"> <div class="progress-container">
<input <input
type="range" type="range"
min="0" min="0"
max="100" max="100"
value={(currentTime / duration) * 100 || 0} value={
($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0
}
on:input={seek} on:input={seek}
class="progress-slider" class="progress-slider"
/> />
</div> </div>
<span class="time-total">{formatTime(duration)}</span> <span class="time-total">{formatTime($musicPlayer.duration)}</span>
</div> </div>
<!-- Volume & Extra --> <!-- Volume & Extra -->
<div class="player-extra"> <div class="player-extra">
<div class="volume-wrapper"> <div class="volume-wrapper">
<button class="control-btn volume-icon" on:click={toggleMute} title="Ses"> <button
{#if isMuted} class="control-btn volume-icon"
on:click={toggleMute}
title="Ses"
>
{#if $musicPlayer.isMuted}
<i class="fa-solid fa-volume-xmark"></i> <i class="fa-solid fa-volume-xmark"></i>
{:else if volume < 0.3} {:else if $musicPlayer.volume < 0.3}
<i class="fa-solid fa-volume-off"></i> <i class="fa-solid fa-volume-off"></i>
{:else if volume < 0.7} {:else if $musicPlayer.volume < 0.7}
<i class="fa-solid fa-volume-low"></i> <i class="fa-solid fa-volume-low"></i>
{:else} {:else}
<i class="fa-solid fa-volume-high"></i> <i class="fa-solid fa-volume-high"></i>
@@ -443,17 +376,17 @@
min="0" min="0"
max="1" max="1"
step="0.01" step="0.01"
bind:value={volume} value={$musicPlayer.volume}
on:input={setVolume} on:input={setPlayerVolume}
class="volume-slider" class="volume-slider"
style="--fill: {volume * 100}%" style="--fill: {$musicPlayer.volume * 100}%"
/> />
</div> </div>
</div> </div>
<a <a
class="control-btn download-btn" class="control-btn download-btn"
href={streamURL(currentTrack)} href={streamURL($musicPlayer.currentTrack)}
download={currentTrack.title} download={$musicPlayer.currentTrack.title}
title="İndir" title="İndir"
> >
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
@@ -618,8 +551,12 @@
} }
@keyframes spin { @keyframes spin {
from { transform: rotate(0deg); } from {
to { transform: rotate(360deg); } transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }
/* === List View === */ /* === List View === */
@@ -690,13 +627,27 @@
animation: equalizer 0.8s ease-in-out infinite; animation: equalizer 0.8s ease-in-out infinite;
} }
.playing-indicator .bar-1 { animation-delay: 0s; height: 8px; } .playing-indicator .bar-1 {
.playing-indicator .bar-2 { animation-delay: 0.2s; height: 12px; } animation-delay: 0s;
.playing-indicator .bar-3 { animation-delay: 0.4s; height: 6px; } height: 8px;
}
.playing-indicator .bar-2 {
animation-delay: 0.2s;
height: 12px;
}
.playing-indicator .bar-3 {
animation-delay: 0.4s;
height: 6px;
}
@keyframes equalizer { @keyframes equalizer {
0%, 100% { transform: scaleY(0.5); } 0%,
50% { transform: scaleY(1); } 100% {
transform: scaleY(0.5);
}
50% {
transform: scaleY(1);
}
} }
.row-thumb { .row-thumb {
@@ -879,8 +830,13 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 179, 51, 0.4); } 0%,
50% { box-shadow: 0 0 0 8px rgba(245, 179, 51, 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 { .card-info {
@@ -1131,7 +1087,11 @@
height: 4px; height: 4px;
border-radius: 2px; border-radius: 2px;
cursor: pointer; 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; position: relative;
} }

View File

@@ -1184,7 +1184,7 @@ async function openVideoAtIndex(index) {
.tv-overlay-content { .tv-overlay-content {
position: relative; position: relative;
width: min(1040px, 94vw); width: min(1040px, 94vw);
max-height: 90vh; max-height: 95vh;
border-radius: 20px; border-radius: 20px;
overflow: hidden; overflow: hidden;
background: rgba(12, 12, 12, 0.5); background: rgba(12, 12, 12, 0.5);
@@ -1222,7 +1222,7 @@ async function openVideoAtIndex(index) {
} }
.detail-poster { .detail-poster {
flex: 0 0 230px; flex: 0 0 183px;
} }
.detail-poster-img { .detail-poster-img {
@@ -1363,7 +1363,7 @@ async function openVideoAtIndex(index) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
max-height: 260px; max-height: 520px;
overflow-y: auto; overflow-y: auto;
padding-right: 6px; padding-right: 6px;
padding-left: 2px; padding-left: 2px;
@@ -1691,11 +1691,11 @@ async function openVideoAtIndex(index) {
} }
.detail-poster { .detail-poster {
flex: 0 0 200px; flex: 0 0 72px;
} }
.episode-list { .episode-list {
max-height: 240px; max-height: 440px;
} }
} }
@@ -1716,7 +1716,7 @@ async function openVideoAtIndex(index) {
} }
.detail-poster { .detail-poster {
flex: 0 0 160px; flex: 0 0 58px;
} }
.detail-title { .detail-title {
@@ -1732,7 +1732,7 @@ async function openVideoAtIndex(index) {
} }
.episode-list { .episode-list {
max-height: 200px; max-height: 360px;
gap: 12px; gap: 12px;
} }
@@ -1789,7 +1789,7 @@ async function openVideoAtIndex(index) {
} }
.detail-poster { .detail-poster {
flex: 0 0 120px; flex: 0 0 43px;
} }
.detail-title { .detail-title {
@@ -1817,7 +1817,7 @@ async function openVideoAtIndex(index) {
} }
.episode-list { .episode-list {
max-height: 180px; max-height: 320px;
gap: 10px; gap: 10px;
} }

View File

@@ -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
};

View File

@@ -1,9 +1,7 @@
version: "3.9"
services: services:
dupe: dupe:
build: . build: .
container_name: app container_name: dupe
ports: ports:
- "3005:3001" - "3005:3001"
volumes: volumes:
@@ -18,3 +16,6 @@ services:
TVDB_API_KEY: ${TVDB_API_KEY} TVDB_API_KEY: ${TVDB_API_KEY}
FANART_TV_API_KEY: ${FANART_TV_API_KEY} FANART_TV_API_KEY: ${FANART_TV_API_KEY}
VIDEO_THUMBNAIL_TIME: ${VIDEO_THUMBNAIL_TIME} VIDEO_THUMBNAIL_TIME: ${VIDEO_THUMBNAIL_TIME}
DEBUG_CPU: ${DEBUG_CPU}
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}

View File

@@ -24,6 +24,9 @@ const torrents = new Map();
const youtubeJobs = new Map(); const youtubeJobs = new Map();
let wss; let wss;
const PORT = process.env.PORT || 3001; 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 --- // --- İndirilen dosyalar için klasör oluştur ---
const DOWNLOAD_DIR = path.join(__dirname, "downloads"); const DOWNLOAD_DIR = path.join(__dirname, "downloads");
@@ -108,10 +111,40 @@ const FFPROBE_MAX_BUFFER =
: 10 * 1024 * 1024; : 10 * 1024 * 1024;
const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png"); 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(cors());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use("/downloads", express.static(DOWNLOAD_DIR)); app.use("/downloads", express.static(DOWNLOAD_DIR));
startCpuProfiler();
// --- En uygun video dosyasını seç --- // --- En uygun video dosyasını seç ---
function pickBestVideoFile(torrent) { function pickBestVideoFile(torrent) {
@@ -1368,6 +1401,7 @@ function bytesFromHuman(value, unit = "B") {
} }
async function extractMediaInfo(filePath, retryCount = 0) { async function extractMediaInfo(filePath, retryCount = 0) {
if (DISABLE_MEDIA_PROCESSING) return null;
if (!filePath || !fs.existsSync(filePath)) return null; if (!filePath || !fs.existsSync(filePath)) return null;
// Farklı ffprobe stratejileri // Farklı ffprobe stratejileri
@@ -1486,6 +1520,7 @@ async function extractMediaInfo(filePath, retryCount = 0) {
} }
function queueVideoThumbnail(fullPath, relPath, retryCount = 0) { function queueVideoThumbnail(fullPath, relPath, retryCount = 0) {
if (DISABLE_MEDIA_PROCESSING) return;
const { relThumb, absThumb } = getVideoThumbnailPaths(relPath); const { relThumb, absThumb } = getVideoThumbnailPaths(relPath);
if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return; if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return;
@@ -1544,6 +1579,7 @@ function queueVideoThumbnail(fullPath, relPath, retryCount = 0) {
} }
function queueImageThumbnail(fullPath, relPath, retryCount = 0) { function queueImageThumbnail(fullPath, relPath, retryCount = 0) {
if (DISABLE_MEDIA_PROCESSING) return;
const { relThumb, absThumb } = getImageThumbnailPaths(relPath); const { relThumb, absThumb } = getImageThumbnailPaths(relPath);
if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return; if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return;
@@ -2398,6 +2434,14 @@ async function ensureMovieData(
bestVideoPath, bestVideoPath,
precomputedMediaInfo = null precomputedMediaInfo = null
) { ) {
if (DISABLE_MEDIA_PROCESSING) {
return {
mediaInfo: precomputedMediaInfo || null,
metadata: null,
cacheKey: null,
videoPath: bestVideoPath ? normalizeTrashPath(bestVideoPath) : null
};
}
const normalizedRoot = sanitizeRelative(rootFolder); const normalizedRoot = sanitizeRelative(rootFolder);
if (!TMDB_API_KEY) { if (!TMDB_API_KEY) {
return { return {
@@ -2910,6 +2954,7 @@ async function ensureSeriesData(
seriesInfo, seriesInfo,
mediaInfo mediaInfo
) { ) {
if (DISABLE_MEDIA_PROCESSING) return null;
if (!TVDB_API_KEY || !seriesInfo) { if (!TVDB_API_KEY || !seriesInfo) {
console.log("📺 TVDB atlandı (eksik anahtar ya da seriesInfo yok):", { console.log("📺 TVDB atlandı (eksik anahtar ya da seriesInfo yok):", {
rootFolder, 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); const legacyPaths = tvSeriesPaths(normalizedRoot);
if (!seriesData && fs.existsSync(legacyPaths.metadata)) { if (!seriesData && fs.existsSync(legacyPaths.metadata)) {
try { try {
@@ -4211,6 +4272,7 @@ let pendingMediaRescan = { movies: false, tv: false };
let lastMediaRescanReason = "manual"; let lastMediaRescanReason = "manual";
function queueMediaRescan({ movies = false, tv = false, reason = "manual" } = {}) { function queueMediaRescan({ movies = false, tv = false, reason = "manual" } = {}) {
if (DISABLE_MEDIA_PROCESSING) return;
if (!movies && !tv) return; if (!movies && !tv) return;
pendingMediaRescan.movies = pendingMediaRescan.movies || movies; pendingMediaRescan.movies = pendingMediaRescan.movies || movies;
pendingMediaRescan.tv = pendingMediaRescan.tv || tv; pendingMediaRescan.tv = pendingMediaRescan.tv || tv;
@@ -4578,7 +4640,7 @@ async function onTorrentDone({ torrent }) {
}; };
const seriesInfo = parseSeriesInfo(file.name); const seriesInfo = parseSeriesInfo(file.name);
if (seriesInfo) { if (seriesInfo && !DISABLE_MEDIA_PROCESSING) {
try { try {
const ensured = await ensureSeriesData( const ensured = await ensureSeriesData(
rootFolder, rootFolder,
@@ -4665,53 +4727,53 @@ async function onTorrentDone({ torrent }) {
infoUpdate.seriesEpisodes = seriesEpisodes; infoUpdate.seriesEpisodes = seriesEpisodes;
} }
const ensuredMedia = await ensureMovieData( if (!DISABLE_MEDIA_PROCESSING) {
rootFolder, const ensuredMedia = await ensureMovieData(
displayName, rootFolder,
bestVideoPath, displayName,
primaryMediaInfo bestVideoPath,
); primaryMediaInfo
if (ensuredMedia?.mediaInfo) { );
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo; if (ensuredMedia?.mediaInfo) {
if (!infoUpdate.files) infoUpdate.files = perFileMetadata; infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
if (bestVideoPath) { if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
const entry = infoUpdate.files[bestVideoPath] || {}; if (bestVideoPath) {
infoUpdate.files[bestVideoPath] = { const fileEntry = infoUpdate.files[bestVideoPath] || {};
...entry, infoUpdate.files[bestVideoPath] = {
movieMatch: ensuredMedia.metadata ...fileEntry,
? { movieMatch: ensuredMedia.metadata
id: ensuredMedia.metadata.id ?? null, ? {
title: id: ensuredMedia.metadata.id ?? null,
ensuredMedia.metadata.title || title:
ensuredMedia.metadata.matched_title || ensuredMedia.metadata.title ||
displayName, ensuredMedia.metadata.matched_title ||
year: ensuredMedia.metadata.release_date displayName,
? Number( year: ensuredMedia.metadata.release_date
ensuredMedia.metadata.release_date.slice(0, 4) ? Number(ensuredMedia.metadata.release_date.slice(0, 4))
) : ensuredMedia.metadata.matched_year || null,
: ensuredMedia.metadata.matched_year || null, poster: ensuredMedia.metadata.poster_path || null,
poster: ensuredMedia.metadata.poster_path || null, backdrop: ensuredMedia.metadata.backdrop_path || null,
backdrop: ensuredMedia.metadata.backdrop_path || null, cacheKey: ensuredMedia.cacheKey || null,
cacheKey: ensuredMedia.cacheKey || null, matchedAt: Date.now()
matchedAt: Date.now() }
} : fileEntry.movieMatch
: entry.movieMatch };
}; const movieType = determineMediaType({
const movieType = determineMediaType({ tracker: torrent.announce?.[0] || null,
tracker: torrent.announce?.[0] || null, movieMatch: ensuredMedia.metadata,
movieMatch: ensuredMedia.metadata, seriesEpisode: seriesEpisodes[bestVideoPath] || null,
seriesEpisode: seriesEpisodes[bestVideoPath] || null, categories: null,
categories: null, relPath: bestVideoPath,
relPath: bestVideoPath, audioOnly: false
audioOnly: false });
}); perFileMetadata[bestVideoPath] = {
perFileMetadata[bestVideoPath] = { ...(perFileMetadata[bestVideoPath] || {}),
...(perFileMetadata[bestVideoPath] || {}), type: movieType
type: movieType };
}; infoUpdate.files[bestVideoPath].type = movieType;
infoUpdate.files[bestVideoPath].type = movieType; }
}
} }
}
upsertInfoFile(entry.savePath, infoUpdate); upsertInfoFile(entry.savePath, infoUpdate);
broadcastFileUpdate(rootFolder); broadcastFileUpdate(rootFolder);
@@ -4719,6 +4781,13 @@ async function onTorrentDone({ torrent }) {
// Torrent tamamlandığında disk space bilgisini güncelle // Torrent tamamlandığında disk space bilgisini güncelle
broadcastDiskSpace(); 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 // Medya tespiti tamamlandığında özel bildirim gönder
if (Object.keys(seriesEpisodes).length > 0 || infoUpdate.primaryMediaInfo) { if (Object.keys(seriesEpisodes).length > 0 || infoUpdate.primaryMediaInfo) {
if (wss) { if (wss) {
@@ -5923,6 +5992,10 @@ app.get("/api/movies", requireAuth, (req, res) => {
}); });
async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = false } = {}) { 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) { if (!TMDB_API_KEY) {
throw new Error("TMDB API key tanımlı değil."); 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 } = {}) { async function rebuildTvMetadata({ clearCache = false } = {}) {
if (DISABLE_MEDIA_PROCESSING) {
console.log("📺 Medya işlemleri kapalı; TV metadata taraması atlandı.");
return [];
}
if (!TVDB_API_KEY) { if (!TVDB_API_KEY) {
throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil."); throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil.");
} }