Compare commits

..

33 Commits

Author SHA1 Message Date
f03de189c8 Merge pull request 'main' (#3) from main into develope
Reviewed-on: #3
2026-01-19 18:11:18 +00:00
1bad4f7256 feat(ui): mini oynatıcıya video önizlemesi ekle
Parça çalınırken video akışı mevcutsa küçük resim yerine video öğesi gösterilir. Video konumu, oynatma zamanı ile senkronize edilir.
2026-01-19 17:37:06 +03:00
d27a4637b0 feat(ui): sayfa başlığını du.pe olarak güncelle 2026-01-19 17:36:15 +03:00
987c698693 Title change 2026-01-18 18:13:31 +03:00
1564edc316 UI Change: Title update 2026-01-18 17:42:01 +03:00
05b95dec64 UI Change: Title update 2026-01-18 17:40:13 +03:00
e7044ac8c2 UI Change: Title update 2026-01-18 17:38:21 +03:00
95f05df4ca UI Change: Title update. 2026-01-18 17:37:21 +03:00
424b2f0c7e UI Update: Title değişti 2026-01-18 17:30:23 +03:00
201480cf62 style(client): başlık "du.pe" yerine "dupe" olarak güncellendi 2026-01-18 17:19:56 +03:00
2eba40c715 style(client): "du.pe" başlığını "dupe" ile güncelle 2026-01-18 17:19:09 +03:00
a722d87f0f title change 2026-01-18 16:38:00 +03:00
c9c7686ef1 fix(readme): "clean" kelimesini "awesome" ile güncelle 2026-01-18 15:34:14 +03:00
d5d9184872 feat(music): mini player ekle
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.
2026-01-18 01:51:15 +03:00
c945458a81 fix(server): seri verisi için aday anahtar kontrolü ekle. TV Shows da dizi bölümlerinin tamamının listelenmesini sağla.
ensureSeriesData fonksiyonuna, veri bulunamadığında candidateKeys listesini kullanarak alternatif dosya yollarının kontrol edilmesi ve ilgili metadatanın yüklenmesi sağlandı.
2026-01-11 14:45:45 +03:00
6cb415687a chore(sidebar): konsol loglarını kaldır 2026-01-10 13:35:16 +03:00
cb9856cf8c feat(config): yapılandırma bayrakları ve cpu profili ekle
DEBUG_CPU, DISABLE_MEDIA_PROCESSING ve AUTO_PAUSE_ON_COMPLETE
seçenekleriyle CPU profili, medya işlem kontrolü ve otomatik
duraklatma özellikleri ekle. WebSocket temizleme işlemini
Sidebar bileşeninde refactor et.
2026-01-10 13:30:07 +03:00
3bda1ed287 feat(config): yeni ortam değişkenleri ekle 2026-01-10 13:28:59 +03:00
0bf6e3bcf3 docs(config): yapılandırma açıklamalarını güncelle 2026-01-10 13:27:52 +03:00
8b64287ad9 chore(docker): konteyner adını 'dupe' olarak güncelle 2026-01-09 17:56:20 +03:00
cad39e5427 feat(music): görünüm modunu localStorage'a kaydet
Kullanıcının liste veya grid görünüm tercihi artık tarayıcıda
saklanacak ve sonraki oturumlarda korunacak.
2026-01-06 23:04:10 +03:00
3078f945a6 fix(music): audio oynatıcıda dom referansını düzelt 2026-01-06 22:58:09 +03:00
127d875350 Merge pull request 'develope' (#2) from develope into main
Reviewed-on: #2
2025-12-31 05:34:32 +00:00
8513d7f2df Logo png olarak güncellendi. 2025-12-20 20:43:10 +00:00
b7adf9ca63 docs: readme başlığını düzelt 2025-12-20 20:32:29 +00:00
92fa8eac4c refactor(deployment): konteyner port maruziyetini değiştir
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.
2025-12-20 20:18:28 +00:00
8cbb060bd2 Merge pull request 'fix(youtube): remove redundant remux-audio parameter from youtube-dl command' (#8) from develope into main
Reviewed-on: #8
2025-12-14 20:27:12 +00:00
6cf2ddbfff Merge pull request 'develope' (#7) from develope into main
Reviewed-on: #7
2025-12-14 20:19:43 +00:00
6aab3940e1 Merge pull request 'feat(youtube): çerez varlığına göre ekstraktör argümanlarını otomatik belirle' (#6) from develope into main
Reviewed-on: #6
2025-12-14 12:57:05 +00:00
c81f312382 Merge pull request 'feat(youtube): youtube extractor argümanlarını yapılandırılabilir yap' (#5) from develope into main
Reviewed-on: #5
2025-12-14 12:47:19 +00:00
f5e4e1a572 Merge pull request 'feat(youtube): youtube çerez yönetimi ekle' (#4) from develope into main
Reviewed-on: #4
2025-12-14 12:39:24 +00:00
dac30820d4 Merge pull request 'feat(youtube): js çalışma zamanını yapılandırılabilir hale getir' (#3) from develope into main
Reviewed-on: #3
2025-12-14 12:19:57 +00:00
18c4460c3c Merge pull request 'feat(youtube): hata ayıklama ve loglama yeteneklerini iyileştir' (#2) from develope into main
Reviewed-on: #2
2025-12-14 12:12:09 +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
# 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 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">
<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>
# du.pe - Simple, Fast & Lightweight Torrent Server ⚡📦
A **self-hosted torrent-based file manager and media player**, similar to Put.io — fast, minimal, and elegant.
Add torrents, monitor downloads, and instantly stream videos through a clean web interface! 🖥️🎬
Add torrents, monitor downloads, and instantly stream videos through a clean awesome web interface! 🖥️🎬
---
## ✨ Features
## ✨ Feature
- 🧲 **Add Torrents**
- Upload `.torrent` files (via form)

View File

@@ -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 @@
<Route path="/trash" component={Trash} />
</div>
<MiniPlayer />
<!-- Sidebar dışına tıklayınca kapanma -->
{#if menuOpen}
<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
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,17 +94,12 @@ 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) {
@@ -112,23 +107,9 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
}
};
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();
}
});
});
</script>

View File

@@ -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 = "";
}
};
});
</script>
@@ -212,14 +135,14 @@
<div class="view-toggle">
<button
class="view-btn {viewMode === 'list' ? 'active' : ''}"
on:click={() => viewMode = 'list'}
on:click={() => setViewMode("list")}
title="Liste görünümü"
>
<i class="fa-solid fa-list"></i>
</button>
<button
class="view-btn {viewMode === 'grid' ? 'active' : ''}"
on:click={() => viewMode = 'grid'}
on:click={() => setViewMode("grid")}
title="Grid görünümü"
>
<i class="fa-solid fa-border-all"></i>
@@ -248,7 +171,7 @@
<i class="fa-solid fa-music"></i>
<p>Henüz müzik dosyası yok.</p>
</div>
{:else if viewMode === 'list'}
{:else if viewMode === "list"}
<!-- List View -->
<div class="music-list">
<div class="list-header">
@@ -261,14 +184,14 @@
</div>
{#each items as item, idx (item.id)}
<div
class="music-row {currentTrack?.id === item.id ? 'playing' : ''}"
class="music-row {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)}
on:dblclick={() => {
if (currentTrack?.id === item.id) togglePlay();
if ($musicPlayer.currentTrack?.id === item.id) togglePlay();
}}
>
<div class="row-index">
{#if currentTrack?.id === item.id && isPlaying}
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
<div class="playing-indicator">
<span class="bar bar-1"></span>
<span class="bar bar-2"></span>
@@ -292,7 +215,9 @@
<div class="row-source">{sourceLabel(item)}</div>
</div>
<div class="row-time">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
{formatDuration(
item.mediaInfo?.format?.duration || item.duration,
)}
</div>
<div class="row-actions" on:click|stopPropagation>
<button
@@ -318,7 +243,7 @@
<div class="music-grid">
{#each items as item, idx (item.id)}
<div
class="music-card {currentTrack?.id === item.id ? 'playing' : ''}"
class="music-card {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)}
>
<div class="card-thumb">
@@ -334,7 +259,7 @@
<i class="fa-solid fa-play"></i>
</button>
</div>
{#if currentTrack?.id === item.id && isPlaying}
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
<div class="playing-badge">
<i class="fa-solid fa-volume-high"></i>
</div>
@@ -344,7 +269,9 @@
<div class="card-title">{cleanFileName(item.title)}</div>
<div class="card-source">{sourceLabel(item)}</div>
<div class="card-duration">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
{formatDuration(
item.mediaInfo?.format?.duration || item.duration,
)}
</div>
</div>
</div>
@@ -354,23 +281,17 @@
</div>
<!-- Bottom Player Bar -->
{#if currentTrack}
{#if $musicPlayer.currentTrack}
<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">
<!-- Track Info -->
<div class="player-track">
<div class="track-thumb">
{#if thumbnailURL(currentTrack)}
<img src={thumbnailURL(currentTrack)} alt={currentTrack.title} />
{#if thumbnailURL($musicPlayer.currentTrack)}
<img
src={thumbnailURL($musicPlayer.currentTrack)}
alt={$musicPlayer.currentTrack.title}
/>
{:else}
<div class="thumb-placeholder">
<i class="fa-solid fa-music"></i>
@@ -378,11 +299,15 @@
{/if}
</div>
<div class="track-info">
<div class="track-name">{cleanFileName(currentTrack.title)}</div>
<div class="track-source">{sourceLabel(currentTrack)}</div>
<div class="track-name">
{cleanFileName($musicPlayer.currentTrack.title)}
</div>
<div class="track-source">
{sourceLabel($musicPlayer.currentTrack)}
</div>
</div>
<button class="like-btn" title="Beğen">
<i class="fa-regular fa-heart"></i>
<button class="like-btn" title="Kapat" on:click={stopPlayback}>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@@ -394,9 +319,9 @@
<button
class="control-btn play-pause-btn"
on:click={togglePlay}
title={isPlaying ? "Durdaklat" : "Oynat"}
title={$musicPlayer.isPlaying ? "Durdaklat" : "Oynat"}
>
{#if isPlaying}
{#if $musicPlayer.isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
@@ -409,29 +334,37 @@
<!-- Progress -->
<div class="player-progress">
<span class="time-current">{formatTime(currentTime)}</span>
<span class="time-current">
{formatTime($musicPlayer.currentTime)}
</span>
<div class="progress-container">
<input
type="range"
min="0"
max="100"
value={(currentTime / duration) * 100 || 0}
value={
($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0
}
on:input={seek}
class="progress-slider"
/>
</div>
<span class="time-total">{formatTime(duration)}</span>
<span class="time-total">{formatTime($musicPlayer.duration)}</span>
</div>
<!-- Volume & Extra -->
<div class="player-extra">
<div class="volume-wrapper">
<button class="control-btn volume-icon" on:click={toggleMute} title="Ses">
{#if isMuted}
<button
class="control-btn volume-icon"
on:click={toggleMute}
title="Ses"
>
{#if $musicPlayer.isMuted}
<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>
{:else if volume < 0.7}
{:else if $musicPlayer.volume < 0.7}
<i class="fa-solid fa-volume-low"></i>
{:else}
<i class="fa-solid fa-volume-high"></i>
@@ -443,17 +376,17 @@
min="0"
max="1"
step="0.01"
bind:value={volume}
on:input={setVolume}
value={$musicPlayer.volume}
on:input={setPlayerVolume}
class="volume-slider"
style="--fill: {volume * 100}%"
style="--fill: {$musicPlayer.volume * 100}%"
/>
</div>
</div>
<a
class="control-btn download-btn"
href={streamURL(currentTrack)}
download={currentTrack.title}
href={streamURL($musicPlayer.currentTrack)}
download={$musicPlayer.currentTrack.title}
title="İndir"
>
<i class="fa-solid fa-download"></i>
@@ -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;
}

View File

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

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:
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}

View File

@@ -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.");
}