diff --git a/.env.example b/.env.example index 1689db7..3d49add 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,8 @@ RCLONE_POLL_INTERVAL=1m # Rclone dizin cache süresi RCLONE_DIR_CACHE_TIME=1m # Rclone VFS cache modu (off, minimal, writes, full) +# full: Hızlı streaming için okumalar ve yazmalar cache'lenir +# Disk doluluğu threshold'ı geçince otomatik temizlenir RCLONE_VFS_CACHE_MODE=full # Rclone VFS cache dizini RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs @@ -73,3 +75,25 @@ RCLONE_RC_ADDR=127.0.0.1:5572 RCLONE_DEBUG_MODE_LOG=0 # Media stream debug log (akış kaynağını loglamak için kullanılır) MEDIA_DEBUG_LOG=0 + +# --- Rclone Streaming Performans Ayarları --- +# Buffer size - streaming performansı için (varsayılan: 16M, VPS için 8M yeterli) +RCLONE_BUFFER_SIZE=8M +# VFS read ahead - streaming için önbellek (varsayılan: off) +RCLONE_VFS_READ_AHEAD=128M +# VFS read chunk size - büyük dosyalar için (varsayılan: 128M) +RCLONE_VFS_READ_CHUNK_SIZE=32M +# VFS read chunk size limit - seek performansı için (varsayılan: off) +RCLONE_VFS_READ_CHUNK_SIZE_LIMIT=64M + +# --- Rclone Akıllı Cache Yönetimi --- +# Disk doluluk oranı eşik değeri (百分比) - Bu oran aşıldığında otomatik cache temizlenir +RCLONE_CACHE_CLEAN_THRESHOLD=85 +# Cache temizleme sırasında korunacak minimum boş alan (GB) +RCLONE_MIN_FREE_SPACE_GB=5 +# Rclone crash olursa otomatik yeniden başlatma (1 = aç, 0 = kapa) +RCLONE_AUTO_RESTART=1 +# Maksimum yeniden başlatma deneme sayısı +RCLONE_AUTO_RESTART_MAX_RETRIES=5 +# Yeniden başlatma arasındaki bekleme süresi (milisaniye) +RCLONE_AUTO_RESTART_DELAY_MS=5000 diff --git a/client/src/routes/Settings.svelte b/client/src/routes/Settings.svelte index 61eb4c5..d17c183 100644 --- a/client/src/routes/Settings.svelte +++ b/client/src/routes/Settings.svelte @@ -236,6 +236,25 @@ } } + async function checkAndCleanCache() { + error = null; + success = null; + try { + const resp = await apiFetch("/api/rclone/cache/check-and-clean", { method: "POST" }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok) { + throw new Error(data?.error || data?.message || `HTTP ${resp.status}`); + } + if (data.cleaned) { + success = data.message || "Cache temizlendi."; + } else { + success = data.message || "Disk durumu iyi, temizleme gerekmedi."; + } + } catch (err) { + error = err?.message || "Cache kontrolü başarısız."; + } + } + onMount(() => { loadCookies(); loadYoutubeSettings(); @@ -404,7 +423,10 @@ /> + @@ -444,11 +466,30 @@ {#if rcloneStatus}
+
Durum:
Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}
Mounted: {rcloneStatus.mounted ? "Evet" : "Hayır"}
Remote: {rcloneStatus.remoteConfigured ? "Hazır" : "Eksik"}
+ {#if rcloneStatus.vfsCacheMode} +
VFS Cache Mode: {rcloneStatus.vfsCacheMode}
+ {/if} + {#if rcloneStatus.diskUsage} +
+
Disk Kullanımı:
+
+ Kullanım: %{rcloneStatus.diskUsage.usedPercent} | + Boş: {rcloneStatus.diskUsage.availableGB.toFixed(1)}GB / + {rcloneStatus.diskUsage.totalGB.toFixed(1)}GB +
+ {#if rcloneStatus.cacheCleanThreshold} +
+ Temizleme eşiği: %{rcloneStatus.cacheCleanThreshold} +
+ {/if} +
+ {/if} {#if rcloneStatus.lastError} -
Son hata: {rcloneStatus.lastError}
+
Son hata: {rcloneStatus.lastError}
{/if}
{/if} @@ -668,6 +709,14 @@ color: #0f7a1f; } + :global(code) { + background: #f0f0f0; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + } + .password-field { position: relative; } diff --git a/docker-compose.yml b/docker-compose.yml index c22ab04..48fa9ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,3 +41,18 @@ services: RCLONE_DIR_CACHE_TIME: ${RCLONE_DIR_CACHE_TIME} RCLONE_VFS_CACHE_MODE: ${RCLONE_VFS_CACHE_MODE} RCLONE_VFS_CACHE_DIR: ${RCLONE_VFS_CACHE_DIR} + RCLONE_VFS_CACHE_MAX_SIZE: ${RCLONE_VFS_CACHE_MAX_SIZE} + RCLONE_VFS_CACHE_MAX_AGE: ${RCLONE_VFS_CACHE_MAX_AGE} + RCLONE_RC_ENABLED: ${RCLONE_RC_ENABLED} + RCLONE_RC_ADDR: ${RCLONE_RC_ADDR} + RCLONE_BUFFER_SIZE: ${RCLONE_BUFFER_SIZE} + RCLONE_VFS_READ_AHEAD: ${RCLONE_VFS_READ_AHEAD} + RCLONE_VFS_READ_CHUNK_SIZE: ${RCLONE_VFS_READ_CHUNK_SIZE} + RCLONE_VFS_READ_CHUNK_SIZE_LIMIT: ${RCLONE_VFS_READ_CHUNK_SIZE_LIMIT} + RCLONE_DEBUG_MODE_LOG: ${RCLONE_DEBUG_MODE_LOG} + MEDIA_DEBUG_LOG: ${MEDIA_DEBUG_LOG} + RCLONE_CACHE_CLEAN_THRESHOLD: ${RCLONE_CACHE_CLEAN_THRESHOLD} + RCLONE_MIN_FREE_SPACE_GB: ${RCLONE_MIN_FREE_SPACE_GB} + RCLONE_AUTO_RESTART: ${RCLONE_AUTO_RESTART} + RCLONE_AUTO_RESTART_MAX_RETRIES: ${RCLONE_AUTO_RESTART_MAX_RETRIES} + RCLONE_AUTO_RESTART_DELAY_MS: ${RCLONE_AUTO_RESTART_DELAY_MS} diff --git a/server/server.js b/server/server.js index 58954c1..1da9dd4 100644 --- a/server/server.js +++ b/server/server.js @@ -64,6 +64,26 @@ const RCLONE_VFS_CACHE_MAX_SIZE = process.env.RCLONE_VFS_CACHE_MAX_SIZE || "20G"; const RCLONE_VFS_CACHE_MAX_AGE = process.env.RCLONE_VFS_CACHE_MAX_AGE || "24h"; +// --- Streaming performans ayarları --- +const RCLONE_BUFFER_SIZE = process.env.RCLONE_BUFFER_SIZE || "8M"; +const RCLONE_VFS_READ_AHEAD = process.env.RCLONE_VFS_READ_AHEAD || "128M"; +const RCLONE_VFS_READ_CHUNK_SIZE = process.env.RCLONE_VFS_READ_CHUNK_SIZE || "32M"; +const RCLONE_VFS_READ_CHUNK_SIZE_LIMIT = process.env.RCLONE_VFS_READ_CHUNK_SIZE_LIMIT || "64M"; +// Disk doluluk oranı eşik değeri (百分比) - Bu oran aşıldığında cache temizlenir +const RCLONE_CACHE_CLEAN_THRESHOLD = + Number(process.env.RCLONE_CACHE_CLEAN_THRESHOLD) || 85; +// Cache temizleme sırasında korunacak minimum boş alan (GB) +const RCLONE_MIN_FREE_SPACE_GB = + Number(process.env.RCLONE_MIN_FREE_SPACE_GB) || 5; +// Auto-restart enable/disable +const RCLONE_AUTO_RESTART = ["1", "true", "yes", "on"].includes( + String(process.env.RCLONE_AUTO_RESTART || "1").toLowerCase() +); +// Auto-restart için retry sayısı ve delay +const RCLONE_AUTO_RESTART_MAX_RETRIES = + Number(process.env.RCLONE_AUTO_RESTART_MAX_RETRIES) || 5; +const RCLONE_AUTO_RESTART_DELAY_MS = + Number(process.env.RCLONE_AUTO_RESTART_DELAY_MS) || 5000; const MEDIA_DEBUG_LOG = ["1", "true", "yes", "on"].includes( String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase() ); @@ -762,6 +782,9 @@ let rcloneProcess = null; let rcloneLastError = null; const rcloneAuthSessions = new Map(); let rcloneCacheCleanTimer = null; +// Auto-restart sayaçları +let rcloneRestartCount = 0; +let rcloneRestartInProgress = false; function logRcloneMoveError(context, error) { if (!error) return; @@ -1029,17 +1052,17 @@ function startRcloneCacheCleanSchedule(minutes) { if (!interval || interval <= 0) return; rcloneCacheCleanTimer = setInterval(() => { if (!RCLONE_RC_ENABLED) return; - fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Query string format kullan + const params = new URLSearchParams(); + params.append("recursive", "true"); + + fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh?${params.toString()}`, { + method: "POST" }) .then((resp) => { if (resp.status === 404) { - return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh?${params.toString()}`, { + method: "POST" }); } return resp; @@ -1057,16 +1080,21 @@ async function runRcloneCacheClean() { const wasRunning = Boolean(rcloneProcess && !rcloneProcess.killed); try { if (wasRunning && RCLONE_RC_ENABLED) { - const resp = await fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Rclone RC API doğru format: Query string kullanılır + // POST /vfs/refresh with form data: recursive=true + const params = new URLSearchParams(); + params.append("recursive", "true"); + + const resp = await fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh?${params.toString()}`, { + method: "POST" }); + if (resp.status === 404) { - const fallback = await fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Fallback for older rclone versions + const fallbackParams = new URLSearchParams(); + fallbackParams.append("recursive", "true"); + const fallback = await fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh?${fallbackParams.toString()}`, { + method: "POST" }); if (!fallback.ok) { const body = await fallback.text(); @@ -1083,6 +1111,7 @@ async function runRcloneCacheClean() { return { ok: false, error: "Rclone RC kapalıyken mount durdurulmadan cache temizlenemez." }; } + // RC kapalıysa dosya sisteminden temizle fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true }); fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true }); return { ok: true, method: "fs", restarted: false }; @@ -1091,6 +1120,64 @@ async function runRcloneCacheClean() { } } +// --- Akıllı cache yönetimi --- + +/** + * Disk alanını kontrol eder ve gerekirse cache temizler + * @returns {Promise<{ok: boolean, cleaned: boolean, diskUsage: object, message: string}>} + */ +async function checkAndCleanCacheIfNeeded() { + try { + const diskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + const usedPercent = diskInfo.usedPercent || 0; + const availableGB = parseFloat(diskInfo.availableGB) || 0; + + const shouldClean = usedPercent >= RCLONE_CACHE_CLEAN_THRESHOLD || availableGB < RCLONE_MIN_FREE_SPACE_GB; + + if (!shouldClean) { + return { + ok: true, + cleaned: false, + diskUsage: { usedPercent, availableGB, threshold: RCLONE_CACHE_CLEAN_THRESHOLD, minFreeGB: RCLONE_MIN_FREE_SPACE_GB }, + message: `Disk durumu iyi (${usedPercent}% kullanılıyor, ${availableGB}GB boş)` + }; + } + + console.warn(`⚠️ Disk doluluk oranı yüksek (${usedPercent}%) veya boş alan az (${availableGB}GB). Cache temizleniyor...`); + + const result = await runRcloneCacheClean(); + + if (result.ok) { + // Temizleme sonrası disk durumunu tekrar kontrol et + const newDiskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + return { + ok: true, + cleaned: true, + diskUsage: { + before: { usedPercent, availableGB }, + after: { usedPercent: newDiskInfo.usedPercent, availableGB: parseFloat(newDiskInfo.availableGB) } + }, + message: `Cache temizlendi. Öncesi: ${usedPercent}%, Sonrası: ${newDiskInfo.usedPercent}%`, + method: result.method + }; + } else { + return { + ok: false, + cleaned: false, + error: result.error, + message: `Cache temizleme başarısız: ${result.error}` + }; + } + } catch (err) { + return { + ok: false, + cleaned: false, + error: err?.message || String(err), + message: `Cache kontrolü başarısız: ${err?.message}` + }; + } +} + function isRcloneMounted(mountDir) { if (!mountDir) return false; try { @@ -1157,6 +1244,14 @@ function startRcloneMount(settings) { RCLONE_VFS_CACHE_MAX_SIZE, "--vfs-cache-max-age", RCLONE_VFS_CACHE_MAX_AGE, + "--buffer-size", + RCLONE_BUFFER_SIZE, + "--vfs-read-ahead", + RCLONE_VFS_READ_AHEAD, + "--vfs-read-chunk-size", + RCLONE_VFS_READ_CHUNK_SIZE, + "--vfs-read-chunk-size-limit", + RCLONE_VFS_READ_CHUNK_SIZE_LIMIT, "--dir-cache-time", RCLONE_DIR_CACHE_TIME, "--poll-interval", @@ -1190,10 +1285,39 @@ function startRcloneMount(settings) { console.warn(`⚠️ rclone: ${msg}`); } }); - rcloneProcess.on("exit", (code) => { + rcloneProcess.on("exit", async (code) => { if (code !== 0) { rcloneLastError = `rclone exit: ${code}`; console.warn(`⚠️ rclone mount durdu (code ${code})`); + + // Auto-restart mekanizması + if (RCLONE_AUTO_RESTART && !rcloneRestartInProgress) { + const settings = loadRcloneSettings(); + + if (settings.autoMount && rcloneRestartCount < RCLONE_AUTO_RESTART_MAX_RETRIES) { + rcloneRestartInProgress = true; + rcloneRestartCount++; + + console.warn(`🔄 Rclone otomatik yeniden başlatılıyor (${rcloneRestartCount}/${RCLONE_AUTO_RESTART_MAX_RETRIES})...`); + + // Bekle ve yeniden başlat + setTimeout(async () => { + const result = startRcloneMount(settings); + if (result.ok) { + console.log(`✅ Rclone başarıyla yeniden başlatıldı.`); + rcloneRestartCount = 0; // Başarılı olunca sayacı sıfırla + } else { + console.error(`❌ Rclone yeniden başlatılamadı: ${result.error}`); + } + rcloneRestartInProgress = false; + }, RCLONE_AUTO_RESTART_DELAY_MS); + } else if (rcloneRestartCount >= RCLONE_AUTO_RESTART_MAX_RETRIES) { + console.error(`❌ Rclone yeniden başlatma sayısı aşıldı (${RCLONE_AUTO_RESTART_MAX_RETRIES}). Otomatik yeniden başlatma devre dışı.`); + } + } + } else { + // Normal exit (code 0) - sayacı sıfırla + rcloneRestartCount = 0; } rcloneProcess = null; }); @@ -9068,6 +9192,20 @@ app.post("/api/youtube/settings", requireAuth, (req, res) => { app.get("/api/rclone/status", requireAuth, async (req, res) => { const settings = loadRcloneSettings(); const mounted = isRcloneMounted(settings.mountDir); + + // Disk durumunu da ekle + let diskUsage = null; + try { + const diskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + diskUsage = { + usedPercent: diskInfo.usedPercent || 0, + availableGB: parseFloat(diskInfo.availableGB) || 0, + totalGB: parseFloat(diskInfo.totalGB) || 0 + }; + } catch (err) { + // Disk bilgisi alınamazsa null kalsın + } + res.json({ enabled: RCLONE_ENABLED, mounted, @@ -9081,7 +9219,18 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => { cacheCleanMinutes: settings.cacheCleanMinutes || 0, configExists: fs.existsSync(settings.configPath), remoteConfigured: rcloneConfigHasRemote(settings.remoteName), - lastError: rcloneLastError || null + lastError: rcloneLastError || null, + // Performans ayarları + vfsCacheMode: RCLONE_VFS_CACHE_MODE, + bufferSize: RCLONE_BUFFER_SIZE, + vfsReadAhead: RCLONE_VFS_READ_AHEAD, + vfsReadChunkSize: RCLONE_VFS_READ_CHUNK_SIZE, + vfsReadChunkSizeLimit: RCLONE_VFS_READ_CHUNK_SIZE_LIMIT, + // Disk kullanımı + diskUsage, + // Cache temizleme threshold + cacheCleanThreshold: RCLONE_CACHE_CLEAN_THRESHOLD, + minFreeSpaceGB: RCLONE_MIN_FREE_SPACE_GB }); }); @@ -9223,6 +9372,15 @@ app.post("/api/rclone/cache/clean", requireAuth, async (req, res) => { return res.json({ ok: true, ...result }); }); +// Akıllı cache kontrolü - disk durumunu kontrol eder ve gerekirse temizler +app.post("/api/rclone/cache/check-and-clean", requireAuth, async (req, res) => { + const result = await checkAndCleanCacheIfNeeded(); + if (!result.ok) { + return res.status(500).json({ ok: false, error: result.error, message: result.message }); + } + return res.json({ ok: true, ...result }); +}); + app.get("/api/rclone/conf", requireAuth, (req, res) => { try { if (!fs.existsSync(RCLONE_CONFIG_PATH)) { @@ -10526,6 +10684,20 @@ if (WEBDAV_ENABLED) { // --- ☁️ Rclone auto mount --- const initialRcloneSettings = loadRcloneSettings(); + +// Başlangıçta disk kontrolü yap - cache temizleme gerekirse yap +if (RCLONE_ENABLED) { + checkAndCleanCacheIfNeeded().then(result => { + if (result.cleaned) { + console.log(`🧹 Başlangıç cache temizlemesi: ${result.message}`); + } else { + console.log(`✅ Disk durumu: ${result.message}`); + } + }).catch(err => { + console.warn(`⚠️ Başlangıç cache kontrolü başarısız: ${err.message}`); + }); +} + if (RCLONE_ENABLED && initialRcloneSettings.autoMount) { const result = startRcloneMount(initialRcloneSettings); if (!result.ok) { @@ -10534,6 +10706,16 @@ if (RCLONE_ENABLED && initialRcloneSettings.autoMount) { } startRcloneCacheCleanSchedule(initialRcloneSettings.cacheCleanMinutes || 0); +// --- Disk alanı izleme - periyodik kontrol (her 5 dakikada bir) --- +setInterval(async () => { + if (RCLONE_ENABLED) { + const result = await checkAndCleanCacheIfNeeded(); + if (result.cleaned) { + console.log(`🧹 Otomatik cache temizlemesi: ${result.message}`); + } + } +}, 5 * 60 * 1000); + // --- ✅ Client build (frontend) dosyalarını sun --- const publicDir = path.join(__dirname, "public"); if (fs.existsSync(publicDir)) {