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)) {