Merge pull request 'main' (#3) from main into develope
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
25
.env.example
25
.env.example
@@ -1,8 +1,33 @@
|
|||||||
|
# Varsayılan giriş kullanıcı adı; ilk açılışta otomatik kullanıcı oluşturmak için kullanılır.
|
||||||
|
# Gerçek ortamda tahmin edilmesi zor bir değer seçmeniz önerilir.
|
||||||
USERNAME=madafaka
|
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
281
client/src/components/MiniPlayer.svelte
Normal file
281
client/src/components/MiniPlayer.svelte
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
203
client/src/stores/musicPlayerStore.js
Normal file
203
client/src/stores/musicPlayerStore.js
Normal 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
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
171
server/server.js
171
server/server.js
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user