Compare commits
3 Commits
8b64287ad9
...
cb9856cf8c
| Author | SHA1 | Date | |
|---|---|---|---|
| cb9856cf8c | |||
| 3bda1ed287 | |||
| 0bf6e3bcf3 |
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,6 +4711,7 @@ async function onTorrentDone({ torrent }) {
|
|||||||
infoUpdate.seriesEpisodes = seriesEpisodes;
|
infoUpdate.seriesEpisodes = seriesEpisodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!DISABLE_MEDIA_PROCESSING) {
|
||||||
const ensuredMedia = await ensureMovieData(
|
const ensuredMedia = await ensureMovieData(
|
||||||
rootFolder,
|
rootFolder,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -4675,9 +4722,9 @@ async function onTorrentDone({ torrent }) {
|
|||||||
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
|
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
|
||||||
if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
|
if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
|
||||||
if (bestVideoPath) {
|
if (bestVideoPath) {
|
||||||
const entry = infoUpdate.files[bestVideoPath] || {};
|
const fileEntry = infoUpdate.files[bestVideoPath] || {};
|
||||||
infoUpdate.files[bestVideoPath] = {
|
infoUpdate.files[bestVideoPath] = {
|
||||||
...entry,
|
...fileEntry,
|
||||||
movieMatch: ensuredMedia.metadata
|
movieMatch: ensuredMedia.metadata
|
||||||
? {
|
? {
|
||||||
id: ensuredMedia.metadata.id ?? null,
|
id: ensuredMedia.metadata.id ?? null,
|
||||||
@@ -4686,16 +4733,14 @@ async function onTorrentDone({ torrent }) {
|
|||||||
ensuredMedia.metadata.matched_title ||
|
ensuredMedia.metadata.matched_title ||
|
||||||
displayName,
|
displayName,
|
||||||
year: ensuredMedia.metadata.release_date
|
year: ensuredMedia.metadata.release_date
|
||||||
? Number(
|
? Number(ensuredMedia.metadata.release_date.slice(0, 4))
|
||||||
ensuredMedia.metadata.release_date.slice(0, 4)
|
|
||||||
)
|
|
||||||
: ensuredMedia.metadata.matched_year || null,
|
: ensuredMedia.metadata.matched_year || null,
|
||||||
poster: ensuredMedia.metadata.poster_path || null,
|
poster: ensuredMedia.metadata.poster_path || null,
|
||||||
backdrop: ensuredMedia.metadata.backdrop_path || null,
|
backdrop: ensuredMedia.metadata.backdrop_path || null,
|
||||||
cacheKey: ensuredMedia.cacheKey || null,
|
cacheKey: ensuredMedia.cacheKey || null,
|
||||||
matchedAt: Date.now()
|
matchedAt: Date.now()
|
||||||
}
|
}
|
||||||
: entry.movieMatch
|
: fileEntry.movieMatch
|
||||||
};
|
};
|
||||||
const movieType = determineMediaType({
|
const movieType = determineMediaType({
|
||||||
tracker: torrent.announce?.[0] || null,
|
tracker: torrent.announce?.[0] || null,
|
||||||
@@ -4711,7 +4756,8 @@ async function onTorrentDone({ torrent }) {
|
|||||||
};
|
};
|
||||||
infoUpdate.files[bestVideoPath].type = movieType;
|
infoUpdate.files[bestVideoPath].type = movieType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
upsertInfoFile(entry.savePath, infoUpdate);
|
upsertInfoFile(entry.savePath, infoUpdate);
|
||||||
broadcastFileUpdate(rootFolder);
|
broadcastFileUpdate(rootFolder);
|
||||||
@@ -4719,6 +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.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user