develope #1
290
ANALYSIS.md
Normal file
290
ANALYSIS.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# du.pe Proje Analiz Raporu
|
||||
|
||||
## 📋 Proje Özeti
|
||||
|
||||
**du.pe**, Put.io benzeri bir **self-hosted torrent tabanlı dosya yöneticisi ve medya oynatıcısıdır**. Kullanıcıların torrent dosyalarını eklemesine, indirmeleri yönetmesine ve videoları doğrudan tarayıcı üzerinden akışla (stream) izlemesine olanak tanır.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Mimari Yapı
|
||||
|
||||
```
|
||||
dupe/
|
||||
├── client/ # Svelte + Vite Frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Sidebar, Topbar, TorrentItem
|
||||
│ │ ├── routes/ # Files, Movies, TV Shows, Music, Transfers, Trash, Profile, Settings
|
||||
│ │ ├── stores/ # Svelte stores (movieStore, tvStore, musicStore, trashStore, avatarStore)
|
||||
│ │ ├── styles/ # Ana CSS dosyası
|
||||
│ │ └── utils/ # API yardımcı fonksiyonları
|
||||
│ └── package.json
|
||||
├── server/ # Node.js + Express Backend
|
||||
│ ├── modules/ # auth.js, health.js, state.js, websocket.js
|
||||
│ ├── utils/ # diskSpace.js
|
||||
│ ├── data/ # users.json
|
||||
│ └── server.js # Ana sunucu dosyası (~7100 satır)
|
||||
├── docs/ # Dokümantasyon
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Teknoloji Yığını
|
||||
|
||||
### Backend (Node.js)
|
||||
- **Express** - REST API sunucusu
|
||||
- **WebTorrent** - Torrent indirme yönetimi
|
||||
- **WebSocket (ws)** - Gerçek zamanlı ilerleme güncellemeleri
|
||||
- **Multer** - Dosya upload işlemleri
|
||||
- **JWT (jsonwebtoken)** - Kimlik doğrulama
|
||||
- **ffmpeg/ffprobe** - Video thumbnail oluşturma ve medya bilgisi çıkarma
|
||||
- **TMDB API** - Film metadatası (poster, backdrop, özet)
|
||||
- **TVDB API** - Dizi metadatası
|
||||
- **Fanart.tv API** - Dizi backdrop görselleri
|
||||
- **yt-dlp** - YouTube video indirme
|
||||
|
||||
### Frontend (Svelte)
|
||||
- **Svelte 4** - UI framework
|
||||
- **svelte-routing** - İstemci tarafı yönlendirme
|
||||
- **Vite** - Build aracı
|
||||
- **FontAwesome** - İkonlar
|
||||
- **WebSocket** - Gerçek zamanlı güncellemeler
|
||||
|
||||
---
|
||||
|
||||
## 📡 API Endpoint'leri
|
||||
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|-----------|
|
||||
| `GET` | `/api/health` | Sağlık kontrolü |
|
||||
| `GET` | `/api/profile` | Profil bilgileri |
|
||||
| `POST` | `/api/profile/avatar` | Avatar yükleme |
|
||||
| `GET` | `/api/torrents` | Aktif torrent listesi |
|
||||
| `POST` | `/api/transfer` | Torrent/magnet ekleme |
|
||||
| `POST` | `/api/youtube/download` | YouTube indirme |
|
||||
| `POST` | `/api/torrents/:hash/select/:index` | Dosya seçimi |
|
||||
| `DELETE` | `/api/torrents/:hash` | Torrent silme |
|
||||
| `POST` | `/api/torrents/:hash/toggle` | Tek torrent durdur/devam |
|
||||
| `POST` | `/api/torrents/toggle-all` | Tüm torrentleri durdur/devam |
|
||||
| `GET` | `/api/files` | Dosya gezgini |
|
||||
| `GET` | `/api/movies` | Film listesi |
|
||||
| `GET` | `/api/tvshows` | Dizi listesi |
|
||||
| `GET` | `/api/music` | Müzik listesi |
|
||||
| `GET` | `/api/trash` | Çöp kutusu |
|
||||
| `POST` | `/api/trash/restore` | Çöpten geri yükle |
|
||||
| `DELETE` | `/api/trash` | Çöpten kalıcı sil |
|
||||
| `POST` | `/api/file/move` | Dosya taşıma |
|
||||
| `GET` | `/api/disk-space` | Disk alanı bilgisi |
|
||||
| `GET` | `/api/search/metadata` | TMDB/TVDB arama |
|
||||
| `GET` | `/stream/:hash` | Video akışı |
|
||||
| `GET` | `/media/:path(*)` | Genel medya sunumu |
|
||||
| `GET` | `/thumbnails/:path(*)` | Thumbnail sunumu |
|
||||
| `WS` | `ws://host/?token=...` | WebSocket bağlantısı |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 WebSocket Mesajları
|
||||
|
||||
```javascript
|
||||
// İlerleme güncellemesi
|
||||
{ type: "progress", torrents: [...] }
|
||||
|
||||
// Dosya güncellemesi
|
||||
{ type: "fileUpdate", path: "rootFolder" }
|
||||
|
||||
// Disk alanı güncellemesi
|
||||
{ type: "diskSpace", data: {...} }
|
||||
|
||||
// Medya tespiti
|
||||
{ type: "mediaDetected", rootFolder, hasSeriesEpisodes, hasMovieMatch }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Temel Özellikler
|
||||
|
||||
### 1. Torrent Yönetimi
|
||||
- `.torrent` dosyası veya magnet link ile ekleme
|
||||
- Gerçek zamanlı indirme ilerleme takibi
|
||||
- Tekil veya toplu durdurma/devam ettirme
|
||||
- Dosya seçimi (birden fazla dosya içeren torrentler için)
|
||||
|
||||
### 2. YouTube İndirme
|
||||
- YouTube URL'sinden video/ses indirme
|
||||
- Çözünürlük seçimi (1080p, 720p, 480p, vb.)
|
||||
- Sadece ses indirme seçeneği
|
||||
- Cookie desteği
|
||||
|
||||
### 3. Medya Yönetimi
|
||||
- **Filmler**: TMDB'den otomatik metadata çekme (poster, backdrop, özet)
|
||||
- **Diziler**: TVDB'den sezon/bölüm bilgileri
|
||||
- **Müzik**: YouTube indirmeleri için müzik kütüphanesi
|
||||
- **Thumbnail**: Otomatik video/resim küçük resim oluşturma
|
||||
|
||||
### 4. Dosya Yönetimi
|
||||
- Dosya gezgini
|
||||
- Dosya taşıma
|
||||
- Çöp kutusu (geri yükleme ve kalıcı silme)
|
||||
- `.ignoreFiles` ile dosya filtreleme
|
||||
|
||||
### 5. Video Oynatıcı
|
||||
- Özel modal video oynatıcı
|
||||
- Altyazı desteği (.srt, .vtt)
|
||||
- Ses kontrolü
|
||||
- Tam ekran desteği
|
||||
- İndirme butonu
|
||||
|
||||
### 6. Kimlik Doğrulama
|
||||
- JWT tabanlı auth
|
||||
- Token refresh mekanizması
|
||||
- Profil yönetimi (avatar yükleme)
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Veri Yapısı
|
||||
|
||||
### info.json (Her torrent klasöründe)
|
||||
```json
|
||||
{
|
||||
"infoHash": "...",
|
||||
"name": "Torrent Adı",
|
||||
"tracker": "...",
|
||||
"added": 1234567890,
|
||||
"folder": "klasor_adi",
|
||||
"completedAt": 1234567890,
|
||||
"type": "movie|tv|music|video",
|
||||
"files": {
|
||||
"dosya.mp4": {
|
||||
"size": 123456789,
|
||||
"extension": "mp4",
|
||||
"mediaInfo": {...},
|
||||
"type": "movie"
|
||||
}
|
||||
},
|
||||
"primaryVideoPath": "dosya.mp4",
|
||||
"primaryMediaInfo": {...},
|
||||
"seriesEpisodes": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### .trash (Çöp yönetimi)
|
||||
```json
|
||||
{
|
||||
"updatedAt": 1234567890,
|
||||
"items": [
|
||||
{
|
||||
"path": "dosya.mp4",
|
||||
"originalPath": "klasor/dosya.mp4",
|
||||
"deletedAt": 1234567890,
|
||||
"isDirectory": false,
|
||||
"type": "video/mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Frontend Store Yapısı
|
||||
|
||||
### Svelte Stores
|
||||
- `movieStore` - Film sayacı
|
||||
- `tvStore` - Dizi sayacı
|
||||
- `musicStore` - Müzik sayacı
|
||||
- `trashStore` - Çöp öğeleri
|
||||
- `avatarStore` - Avatar URL
|
||||
- `searchStore` - Arama sorgusu
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Çalışma Mantığı
|
||||
|
||||
1. **Başlangıç**: Sunucu başladığında mevcut torrentler diskten yüklenir
|
||||
2. **Torrent Ekleme**: `.torrent` veya magnet → WebTorrent client'e ekle → WebSocket ile ilerleme bildirimi
|
||||
3. **İndirme Tamamlandı**:
|
||||
- Video thumbnail'ları oluşturulur
|
||||
- Medya bilgisi (ffprobe) çıkarılır
|
||||
- TMDB/TVDB'den metadata çekilir
|
||||
- info.json güncellenir
|
||||
4. **Frontend**: WebSocket üzerinden gerçek zamanlı güncellemeler alır, UI'yi günceller
|
||||
5. **Video Oynatma**: `/stream/:hash` endpoint'i üzerinden video akışı sağlanır
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Güvenlik
|
||||
|
||||
- JWT tabanlı kimlik doğrulama
|
||||
- Token refresh mekanizması
|
||||
- Path sanitization (traversal koruması)
|
||||
- Dosya tipi ve boyut kısıtlamaları
|
||||
- CORS yapılandırması
|
||||
|
||||
---
|
||||
|
||||
## 📦 Docker Entegrasyonu
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
dupe:
|
||||
build: .
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./server/downloads:/app/server/downloads
|
||||
- ./server/cache:/app/server/cache
|
||||
environment:
|
||||
- TMDB_API_KEY
|
||||
- TVDB_API_KEY
|
||||
- YT_DLP_BIN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI/UX Özellikleri
|
||||
|
||||
- Responsive tasarım (mobil uyumlu)
|
||||
- Hamburger menü (mobile)
|
||||
- Drag & drop torrent yükleme
|
||||
- Modal video oynatıcı
|
||||
- İlerleme çubukları
|
||||
- Thumbnail önizlemeler
|
||||
- Gerçek zamanlı hız göstergeleri
|
||||
|
||||
---
|
||||
|
||||
## 📂 Önemli Dosyalar
|
||||
|
||||
| Dosya | Açıklama |
|
||||
|-------|----------|
|
||||
| `server/server.js` | Ana backend sunucusu (~7100 satır) |
|
||||
| `client/src/App.svelte` | Ana uygulama bileşeni |
|
||||
| `client/src/routes/Transfers.svelte` | Torrent yönetim arayüzü |
|
||||
| `client/src/utils/api.js` | API yardımcı fonksiyonları |
|
||||
| `client/src/stores/*.js` | Svelte state yönetimi |
|
||||
| `server/modules/auth.js` | Kimlik doğrulama modülü |
|
||||
| `server/modules/websocket.js` | WebSocket sunucusu |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Geliştirme Notları
|
||||
|
||||
### Ortam Değişkenleri
|
||||
```bash
|
||||
PORT=3001
|
||||
TMDB_API_KEY=...
|
||||
TVDB_API_KEY=...
|
||||
FANART_TV_API_KEY=...
|
||||
YT_DLP_BIN=/usr/local/bin/yt-dlp
|
||||
YT_DLP_COOKIES=/path/to/cookies.txt
|
||||
VITE_API=http://localhost:3001
|
||||
```
|
||||
|
||||
### Bağımlılıklar
|
||||
- **ffmpeg** ve **ffprobe** kurulu olmalıdır
|
||||
- **yt-dlp** (YouTube indirmeleri için)
|
||||
- **Node.js** v18+
|
||||
|
||||
---
|
||||
|
||||
*Rapor oluşturma tarihi: 24 Aralık 2024*
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ services:
|
||||
build: .
|
||||
container_name: app
|
||||
ports:
|
||||
- "3001:3001"
|
||||
- "3005:3001"
|
||||
volumes:
|
||||
- ./downloads:/app/server/downloads
|
||||
- ./cache:/app/server/cache
|
||||
|
||||
272
server/server.js
272
server/server.js
@@ -8,7 +8,7 @@ import mime from "mime-types";
|
||||
import { fileURLToPath } from "url";
|
||||
import { exec, spawn } from "child_process";
|
||||
import crypto from "crypto"; // 🔒 basit token üretimi için
|
||||
import { getSystemDiskInfo } from "./utils/diskSpace.js";
|
||||
import { getDiskSpace, getDownloadsSize } from "./utils/diskSpace.js";
|
||||
import { createAuth } from "./modules/auth.js";
|
||||
import { buildHealthReport, healthRouter } from "./modules/health.js";
|
||||
import { restoreTorrentsFromDisk } from "./modules/state.js";
|
||||
@@ -735,7 +735,7 @@ function startYoutubeDownload(url) {
|
||||
youtubeJobs.set(job.id, job);
|
||||
launchYoutubeJob(job);
|
||||
console.log(`▶️ YouTube indirmesi başlatıldı: ${job.url}`);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
return job;
|
||||
}
|
||||
|
||||
@@ -888,7 +888,7 @@ function launchYoutubeJob(job) {
|
||||
binary,
|
||||
args
|
||||
});
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -922,7 +922,7 @@ function startYoutubeStage(job, fileName) {
|
||||
};
|
||||
job.currentStage = stage;
|
||||
job.stages.push(stage);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
|
||||
function updateYoutubeProgress(job, match) {
|
||||
@@ -959,7 +959,7 @@ function updateYoutubeProgress(job, match) {
|
||||
if (Number.isFinite(speedValue) && speedUnit) {
|
||||
job.downloadSpeed = bytesFromHuman(speedValue, speedUnit);
|
||||
}
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
|
||||
async function finalizeYoutubeJob(job, exitCode) {
|
||||
@@ -979,7 +979,7 @@ async function finalizeYoutubeJob(job, exitCode) {
|
||||
args: job.debug?.args,
|
||||
lastLines: tail
|
||||
});
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
return;
|
||||
}
|
||||
if (exitCode !== 0 && fallbackMedia) {
|
||||
@@ -1007,7 +1007,7 @@ async function finalizeYoutubeJob(job, exitCode) {
|
||||
savePath: job.savePath,
|
||||
lastLines: job.debug?.logs?.slice(-8) || []
|
||||
});
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1065,13 +1065,13 @@ async function finalizeYoutubeJob(job, exitCode) {
|
||||
primaryMediaInfo: mediaInfo
|
||||
});
|
||||
broadcastFileUpdate(job.folderId);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
broadcastDiskSpace();
|
||||
console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`);
|
||||
} catch (err) {
|
||||
job.state = "error";
|
||||
job.error = err?.message || "YouTube indirimi tamamlanamadı";
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,7 +1248,7 @@ function removeYoutubeJob(jobId, { removeFiles = true } = {}) {
|
||||
console.warn("YT cache silinemedi:", err.message);
|
||||
}
|
||||
}
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
if (filesRemoved) {
|
||||
broadcastFileUpdate(job.folderId);
|
||||
broadcastDiskSpace();
|
||||
@@ -4104,25 +4104,108 @@ function broadcastFileUpdate(rootFolder) {
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
|
||||
function broadcastDiskSpace() {
|
||||
if (!wss) return;
|
||||
getSystemDiskInfo(DOWNLOAD_DIR).then(diskInfo => {
|
||||
const data = JSON.stringify({
|
||||
type: "diskSpace",
|
||||
data: diskInfo
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}).catch(err => {
|
||||
console.error("❌ Disk space broadcast error:", err.message);
|
||||
});
|
||||
const DISK_SPACE_CACHE_TTL_MS = 30000;
|
||||
const DOWNLOADS_SIZE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
let diskSpaceCache = { value: null, fetchedAt: 0 };
|
||||
let downloadsSizeCache = { value: null, fetchedAt: 0 };
|
||||
let diskInfoInFlight = null;
|
||||
let lastDiskSpacePayload = null;
|
||||
|
||||
async function getCachedDiskInfo({ force = false } = {}) {
|
||||
const now = Date.now();
|
||||
const diskFresh =
|
||||
!force && diskSpaceCache.value && now - diskSpaceCache.fetchedAt < DISK_SPACE_CACHE_TTL_MS;
|
||||
const downloadsFresh =
|
||||
!force &&
|
||||
downloadsSizeCache.value &&
|
||||
now - downloadsSizeCache.fetchedAt < DOWNLOADS_SIZE_CACHE_TTL_MS;
|
||||
|
||||
if (diskFresh && downloadsFresh) {
|
||||
return {
|
||||
...diskSpaceCache.value,
|
||||
downloads: downloadsSizeCache.value,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
if (diskInfoInFlight) return diskInfoInFlight;
|
||||
|
||||
diskInfoInFlight = (async () => {
|
||||
const diskSpace = diskFresh
|
||||
? diskSpaceCache.value
|
||||
: await getDiskSpace(DOWNLOAD_DIR);
|
||||
if (!diskFresh) {
|
||||
diskSpaceCache = { value: diskSpace, fetchedAt: now };
|
||||
}
|
||||
|
||||
const downloadsSize = downloadsFresh
|
||||
? downloadsSizeCache.value
|
||||
: await getDownloadsSize(DOWNLOAD_DIR);
|
||||
if (!downloadsFresh) {
|
||||
downloadsSizeCache = { value: downloadsSize, fetchedAt: now };
|
||||
}
|
||||
|
||||
return {
|
||||
...diskSpace,
|
||||
downloads: downloadsSize,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
})();
|
||||
|
||||
try {
|
||||
return await diskInfoInFlight;
|
||||
} finally {
|
||||
diskInfoInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastDiskSpace() {
|
||||
if (!wss || !hasActiveWsClients()) return;
|
||||
getCachedDiskInfo()
|
||||
.then((diskInfo) => {
|
||||
const data = JSON.stringify({
|
||||
type: "diskSpace",
|
||||
data: diskInfo
|
||||
});
|
||||
if (data === lastDiskSpacePayload) return;
|
||||
lastDiskSpacePayload = data;
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("❌ Disk space broadcast error:", err.message);
|
||||
});
|
||||
}
|
||||
|
||||
let lastSnapshotPayload = null;
|
||||
const SNAPSHOT_DEBOUNCE_MS = 1000;
|
||||
let snapshotTimer = null;
|
||||
let lastSnapshotAt = 0;
|
||||
|
||||
function broadcastSnapshot() {
|
||||
if (!wss) return;
|
||||
if (!wss || !hasActiveWsClients()) return;
|
||||
const data = JSON.stringify({ type: "progress", torrents: snapshot() });
|
||||
if (data === lastSnapshotPayload) return;
|
||||
lastSnapshotPayload = data;
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
|
||||
function scheduleSnapshotBroadcast() {
|
||||
if (!wss || !hasActiveWsClients()) return;
|
||||
const now = Date.now();
|
||||
const remaining = SNAPSHOT_DEBOUNCE_MS - (now - lastSnapshotAt);
|
||||
if (remaining <= 0) {
|
||||
lastSnapshotAt = now;
|
||||
broadcastSnapshot();
|
||||
return;
|
||||
}
|
||||
if (snapshotTimer) return;
|
||||
snapshotTimer = setTimeout(() => {
|
||||
snapshotTimer = null;
|
||||
lastSnapshotAt = Date.now();
|
||||
broadcastSnapshot();
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
let mediaRescanTask = null;
|
||||
let pendingMediaRescan = { movies: false, tv: false };
|
||||
let lastMediaRescanReason = "manual";
|
||||
@@ -4252,22 +4335,73 @@ function inferMediaFlagsFromTrashEntry(entry) {
|
||||
return { movies: true, tv: false };
|
||||
}
|
||||
|
||||
const THUMBNAIL_CHECK_INTERVAL_MS = 15000;
|
||||
|
||||
function hasActiveWsClients() {
|
||||
if (!wss) return false;
|
||||
let hasActive = false;
|
||||
wss.clients.forEach((c) => {
|
||||
if (c.readyState === 1) hasActive = true;
|
||||
});
|
||||
return hasActive;
|
||||
}
|
||||
|
||||
function ensureTorrentSnapshotCache(entry) {
|
||||
if (!entry) return entry;
|
||||
const { torrent, savePath } = entry;
|
||||
if (!entry.rootFolder && savePath) entry.rootFolder = path.basename(savePath);
|
||||
|
||||
if (!entry.filesSnapshot && Array.isArray(torrent?.files)) {
|
||||
entry.filesSnapshot = torrent.files.map((f, i) => ({
|
||||
index: i,
|
||||
name: f.name,
|
||||
length: f.length
|
||||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
entry.bestVideoIndex === undefined ||
|
||||
entry.bestVideoIndex === null
|
||||
) {
|
||||
entry.bestVideoIndex = pickBestVideoFile(torrent);
|
||||
}
|
||||
|
||||
const bestVideo =
|
||||
torrent?.files?.[entry.bestVideoIndex] || torrent?.files?.[0];
|
||||
if (!bestVideo || !savePath || !entry.rootFolder) return entry;
|
||||
|
||||
const now = Date.now();
|
||||
const shouldCheckThumbnail =
|
||||
!entry.thumbnail ||
|
||||
!entry.thumbnailCheckedAt ||
|
||||
now - entry.thumbnailCheckedAt > THUMBNAIL_CHECK_INTERVAL_MS;
|
||||
|
||||
if (shouldCheckThumbnail) {
|
||||
const relPath = path.join(entry.rootFolder, bestVideo.path);
|
||||
const { relThumb, absThumb } = getVideoThumbnailPaths(relPath);
|
||||
entry.thumbnailCheckedAt = now;
|
||||
entry.thumbnailRelPath = relThumb;
|
||||
|
||||
if (fs.existsSync(absThumb)) {
|
||||
entry.thumbnail = thumbnailUrl(relThumb);
|
||||
entry.thumbnailQueued = false;
|
||||
} else if (torrent?.progress === 1 || torrent?.done) {
|
||||
if (!entry.thumbnailQueued) {
|
||||
queueVideoThumbnail(path.join(savePath, bestVideo.path), relPath);
|
||||
entry.thumbnailQueued = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
// --- Snapshot (thumbnail dahil, tracker + tarih eklendi) ---
|
||||
function snapshot() {
|
||||
const torrentEntries = Array.from(torrents.values()).map(
|
||||
({ torrent, selectedIndex, savePath, added, paused }) => {
|
||||
const rootFolder = path.basename(savePath);
|
||||
const bestVideoIndex = pickBestVideoFile(torrent);
|
||||
const bestVideo = torrent.files[bestVideoIndex];
|
||||
let thumbnail = null;
|
||||
|
||||
if (bestVideo) {
|
||||
const relPath = path.join(rootFolder, bestVideo.path);
|
||||
const { relThumb, absThumb } = getVideoThumbnailPaths(relPath);
|
||||
if (fs.existsSync(absThumb)) thumbnail = thumbnailUrl(relThumb);
|
||||
else if (torrent.progress === 1)
|
||||
queueVideoThumbnail(path.join(savePath, bestVideo.path), relPath);
|
||||
}
|
||||
const torrentEntries = Array.from(torrents.values()).map((entry) => {
|
||||
const { torrent, selectedIndex, savePath, added, paused } = entry;
|
||||
ensureTorrentSnapshotCache(entry);
|
||||
const rootFolder = entry?.rootFolder || path.basename(savePath);
|
||||
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
@@ -4282,13 +4416,9 @@ function snapshot() {
|
||||
added,
|
||||
savePath, // 🆕 BURASI!
|
||||
paused: paused || false, // Pause durumunu ekle
|
||||
files: torrent.files.map((f, i) => ({
|
||||
index: i,
|
||||
name: f.name,
|
||||
length: f.length
|
||||
})),
|
||||
files: entry?.filesSnapshot || [],
|
||||
selectedIndex,
|
||||
thumbnail
|
||||
thumbnail: entry?.thumbnail || null
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -4308,9 +4438,24 @@ function wireTorrent(torrent, { savePath, added, respond, restored = false }) {
|
||||
selectedIndex: 0,
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
paused: false,
|
||||
filesSnapshot: null,
|
||||
thumbnail: null,
|
||||
thumbnailCheckedAt: 0,
|
||||
thumbnailQueued: false,
|
||||
bestVideoIndex: null,
|
||||
rootFolder: savePath ? path.basename(savePath) : null
|
||||
});
|
||||
|
||||
const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast();
|
||||
torrent.on("download", scheduleTorrentSnapshot);
|
||||
torrent.on("upload", scheduleTorrentSnapshot);
|
||||
torrent.on("wire", scheduleTorrentSnapshot);
|
||||
torrent.on("noPeers", scheduleTorrentSnapshot);
|
||||
torrent.on("metadata", scheduleTorrentSnapshot);
|
||||
torrent.on("warning", scheduleTorrentSnapshot);
|
||||
torrent.on("error", scheduleTorrentSnapshot);
|
||||
|
||||
torrent.on("ready", () => {
|
||||
onTorrentReady({ torrent, savePath, added, respond, restored });
|
||||
});
|
||||
@@ -4322,12 +4467,19 @@ function wireTorrent(torrent, { savePath, added, respond, restored = false }) {
|
||||
|
||||
function onTorrentReady({ torrent, savePath, added, respond }) {
|
||||
const selectedIndex = pickBestVideoFile(torrent);
|
||||
const existing = torrents.get(torrent.infoHash) || {};
|
||||
torrents.set(torrent.infoHash, {
|
||||
torrent,
|
||||
selectedIndex,
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
paused: false,
|
||||
filesSnapshot: existing.filesSnapshot || null,
|
||||
thumbnail: existing.thumbnail || null,
|
||||
thumbnailCheckedAt: existing.thumbnailCheckedAt || 0,
|
||||
thumbnailQueued: existing.thumbnailQueued || false,
|
||||
bestVideoIndex: selectedIndex,
|
||||
rootFolder: savePath ? path.basename(savePath) : existing.rootFolder || null
|
||||
});
|
||||
const rootFolder = path.basename(savePath);
|
||||
upsertInfoFile(savePath, {
|
||||
@@ -4356,7 +4508,7 @@ function onTorrentReady({ torrent, savePath, added, respond }) {
|
||||
};
|
||||
|
||||
if (typeof respond === "function") respond(payload);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
|
||||
async function onTorrentDone({ torrent }) {
|
||||
@@ -4594,7 +4746,7 @@ async function onTorrentDone({ torrent }) {
|
||||
infoUpdate.files[bestVideoPath].type || rootType;
|
||||
}
|
||||
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
|
||||
// Auth router ve middleware createAuth ile yüklendi
|
||||
@@ -4741,7 +4893,7 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
|
||||
`ℹ️ ${req.params.hash} torrent'i tamamlandığı için yalnızca Transfers listesinden kaldırıldı; dosyalar tutuldu.`
|
||||
);
|
||||
}
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
res.json({
|
||||
ok: true,
|
||||
filesRemoved: !isComplete
|
||||
@@ -4877,7 +5029,7 @@ app.post("/api/torrents/toggle-all", requireAuth, (req, res) => {
|
||||
|
||||
global.pausedTorrents = pausedTorrents;
|
||||
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
res.json({
|
||||
ok: true,
|
||||
action,
|
||||
@@ -4935,7 +5087,7 @@ app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => {
|
||||
}
|
||||
global.pausedTorrents = pausedTorrents;
|
||||
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
res.json({
|
||||
ok: true,
|
||||
action,
|
||||
@@ -5106,16 +5258,16 @@ app.delete("/api/file", requireAuth, (req, res) => {
|
||||
entry?.torrent?.destroy(() => {
|
||||
torrents.delete(matchedInfoHash);
|
||||
console.log(`🧹 Torrent kaydı da temizlendi: ${matchedInfoHash}`);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
|
||||
// Torrent silindiğinde disk space bilgisini güncelle
|
||||
broadcastDiskSpace();
|
||||
});
|
||||
} else {
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
} else {
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -6575,7 +6727,8 @@ function collectMusicEntries() {
|
||||
? `https://www.youtube.com/watch?v=${fileMeta.youtube.videoId}`
|
||||
: null,
|
||||
thumbnail,
|
||||
categories: metadata?.categories || fileMeta?.categories || null
|
||||
categories: metadata?.categories || fileMeta?.categories || null,
|
||||
mediaInfo: fileMeta?.mediaInfo || null
|
||||
});
|
||||
}
|
||||
entries.sort((a, b) => (b.added || 0) - (a.added || 0));
|
||||
@@ -6957,13 +7110,6 @@ wss.on("connection", (ws) => {
|
||||
broadcastDiskSpace();
|
||||
});
|
||||
|
||||
// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla ---
|
||||
setInterval(() => {
|
||||
if (torrents.size > 0) {
|
||||
broadcastSnapshot();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// --- ⏱️ Her 30 saniyede bir disk space bilgisi yayınla ---
|
||||
setInterval(() => {
|
||||
broadcastDiskSpace();
|
||||
@@ -6977,7 +7123,9 @@ app.get("/api/disk-space", requireAuth, async (req, res) => {
|
||||
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const diskInfo = await getSystemDiskInfo(DOWNLOAD_DIR);
|
||||
const diskInfo = await getCachedDiskInfo({
|
||||
force: req.query?.fresh === "1"
|
||||
});
|
||||
res.json(diskInfo);
|
||||
} catch (err) {
|
||||
console.error("❌ Disk space error:", err.message);
|
||||
@@ -7567,7 +7715,7 @@ app.patch("/api/folder", requireAuth, (req, res) => {
|
||||
console.log(`📁 Kök klasör yeniden adlandırıldı: ${rootFolder} -> ${newRootFolder}`);
|
||||
broadcastFileUpdate(rootFolder);
|
||||
broadcastFileUpdate(newRootFolder);
|
||||
broadcastSnapshot();
|
||||
scheduleSnapshotBroadcast();
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Klasör yeniden adlandırıldı",
|
||||
|
||||
Reference in New Issue
Block a user