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 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 anahtarı; ekstra görseller/arka planlar için kullanılır.
# Boşsa fanart görselleri yüklenmez.
FANART_TV_API_KEY=".." 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,17 +94,12 @@ 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) {
@@ -112,23 +107,9 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
} }
}; };
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>
<button class="like-btn" title="Beğen"> <div class="track-source">
<i class="fa-regular fa-heart"></i> {sourceLabel($musicPlayer.currentTrack)}
</div>
</div>
<button class="like-btn" title="Kapat" on:click={stopPlayback}>
<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,6 +4727,7 @@ async function onTorrentDone({ torrent }) {
infoUpdate.seriesEpisodes = seriesEpisodes; infoUpdate.seriesEpisodes = seriesEpisodes;
} }
if (!DISABLE_MEDIA_PROCESSING) {
const ensuredMedia = await ensureMovieData( const ensuredMedia = await ensureMovieData(
rootFolder, rootFolder,
displayName, displayName,
@@ -4675,9 +4738,9 @@ async function onTorrentDone({ torrent }) {
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo; infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
if (!infoUpdate.files) infoUpdate.files = perFileMetadata; if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
if (bestVideoPath) { if (bestVideoPath) {
const entry = infoUpdate.files[bestVideoPath] || {}; const fileEntry = infoUpdate.files[bestVideoPath] || {};
infoUpdate.files[bestVideoPath] = { infoUpdate.files[bestVideoPath] = {
...entry, ...fileEntry,
movieMatch: ensuredMedia.metadata movieMatch: ensuredMedia.metadata
? { ? {
id: ensuredMedia.metadata.id ?? null, id: ensuredMedia.metadata.id ?? null,
@@ -4686,16 +4749,14 @@ async function onTorrentDone({ torrent }) {
ensuredMedia.metadata.matched_title || ensuredMedia.metadata.matched_title ||
displayName, displayName,
year: ensuredMedia.metadata.release_date year: ensuredMedia.metadata.release_date
? Number( ? Number(ensuredMedia.metadata.release_date.slice(0, 4))
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()
} }
: entry.movieMatch : fileEntry.movieMatch
}; };
const movieType = determineMediaType({ const movieType = determineMediaType({
tracker: torrent.announce?.[0] || null, tracker: torrent.announce?.[0] || null,
@@ -4711,7 +4772,8 @@ async function onTorrentDone({ torrent }) {
}; };
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.");
} }