Compare commits

..

3 Commits

Author SHA1 Message Date
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
4 changed files with 146 additions and 60 deletions

View File

@@ -1,8 +1,33 @@
# Varsayılan giriş kullanıcı adı; ilk açılışta otomatik kullanıcı oluşturmak için kullanılır.
# Gerçek ortamda tahmin edilmesi zor bir değer seçmeniz önerilir.
USERNAME=madafaka USERNAME=madafaka
# Varsayılan giriş parolası; ilk kullanıcı oluşturulduktan sonra değiştirilmesi önerilir.
# Güvenlik için güçlü ve benzersiz bir parola kullanın.
PASSWORD=superpassword PASSWORD=superpassword
# JWT erişim tokeni geçerlilik süresi; örn: 15m, 1h gibi değerler alır.
# Çok uzun tutulursa güvenlik riski artar, çok kısa tutulursa kullanıcı oturumu sık yenilenir.
JWT_TTL=15m JWT_TTL=15m
# Frontend'in backend API adresi; farklı makinelerde çalıştırıyorsanız doğru host/port girin.
# Boş bırakılırsa tarayıcı mevcut origin'i kullanır.
VITE_API=http://localhost:3001 VITE_API=http://localhost:3001
# TMDB API anahtarı; film metadata (poster, başlık, özet vb.) çekmek için gereklidir.
# Boşsa film eşleştirme ve zenginleştirme işlemleri çalışmaz.
TMDB_API_KEY="..." TMDB_API_KEY="..."
# TVDB API anahtarı; dizi/episode metadata eşleştirmesi için gereklidir.
# Boşsa dizi verileri ve bölüm detayları oluşturulmaz.
TVDB_API_KEY="..." TVDB_API_KEY="..."
# Video thumbnail almak için kullanılacak zaman noktası; ffmpeg -ss parametresine gider.
# Örn: 10 (saniye) veya 00:00:05 biçiminde ayarlanabilir.
VIDEO_THUMBNAIL_TIME=10 VIDEO_THUMBNAIL_TIME=10
FANART_TV_API_KEY=".." # Fanart.tv API anahtarı; ekstra görseller/arka planlar için kullanılır.
# Boşsa fanart görselleri yüklenmez.
FANART_TV_API_KEY=".."
# Debug amaçlı CPU kullanımını periyodik olarak loglar; yalnızca teşhis için açın,
# üretim ortamında açık bırakmanız log gürültüsü oluşturur.
DEBUG_CPU=0
# Torrent tamamlandığında otomatik pause eder; seeding ve arka plan ağ trafiği azalır,
# CPU tüketimini düşürmeye yardımcı olur. Manuel devam ettirmek istersen kapatın.
AUTO_PAUSE_ON_COMPLETE=0
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
DISABLE_MEDIA_PROCESSING=0

View File

@@ -22,6 +22,7 @@ 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 => {
@@ -59,6 +60,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
@@ -96,10 +100,9 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
const wsUrl = `${wsProtocol}//${wsHost}`; const wsUrl = `${wsProtocol}//${wsHost}`;
console.log('🔌 Connecting to WebSocket at:', wsUrl); console.log('🔌 Connecting to WebSocket at:', wsUrl);
// WebSocket bağlantısını global olarak saklayalım diskSpaceWs = new WebSocket(wsUrl);
window.diskSpaceWs = new WebSocket(wsUrl);
window.diskSpaceWs.onmessage = (event) => { diskSpaceWs.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('WebSocket message received:', data); console.log('WebSocket message received:', data);
@@ -112,23 +115,17 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
} }
}; };
window.diskSpaceWs.onopen = () => { diskSpaceWs.onopen = () => {
console.log('WebSocket connected for disk space updates'); console.log('WebSocket connected for disk space updates');
}; };
window.diskSpaceWs.onerror = (error) => { diskSpaceWs.onerror = (error) => {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
}; };
window.diskSpaceWs.onclose = () => { diskSpaceWs.onclose = () => {
console.log('WebSocket disconnected'); 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

@@ -16,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,
@@ -4211,6 +4256,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 +4624,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 +4711,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 +4765,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 +5976,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 +6803,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.");
} }