+
{#if t.type === "mailru" && t.status === "awaiting_match"}
Eşleştirme bekleniyor
- {:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "moving"}
- GDrive'a Taşınıyor.. • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
+ {:else if t.moveToGdrive && t.moveStatus === "queued"}
+ GDrive kuyruğunda • {(t.downloaded / 1e6).toFixed(1)} MB
+ {:else if t.moveToGdrive && t.moveStatus === "uploading"}
+ GDrive Upload.. • {((t.moveProgress || 0) * 100).toFixed(1)}% • {(t.downloaded / 1e6).toFixed(1)} MB
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "done"}
GDrive ✓ • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "error"}
@@ -1235,6 +1237,10 @@
transition: width 0.3s;
}
+ .progress-bar.uploading .progress {
+ background: linear-gradient(90deg, #ef4444, #b91c1c);
+ }
+
.torrent-error {
color: #e74c3c;
font-size: 12px;
diff --git a/server/server.js b/server/server.js
index 13dec99..00ee887 100644
--- a/server/server.js
+++ b/server/server.js
@@ -56,6 +56,14 @@ const RCLONE_VFS_CACHE_MODE =
const RCLONE_DEBUG_MODE_LOG = ["1", "true", "yes", "on"].includes(
String(process.env.RCLONE_DEBUG_MODE_LOG || "").toLowerCase()
);
+const RCLONE_RC_ENABLED = ["1", "true", "yes", "on"].includes(
+ String(process.env.RCLONE_RC_ENABLED || "1").toLowerCase()
+);
+const RCLONE_RC_ADDR = process.env.RCLONE_RC_ADDR || "127.0.0.1:5572";
+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";
const MEDIA_DEBUG_LOG = ["1", "true", "yes", "on"].includes(
String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase()
);
@@ -753,6 +761,7 @@ function resolveRootDir(rootFolder) {
let rcloneProcess = null;
let rcloneLastError = null;
const rcloneAuthSessions = new Map();
+let rcloneCacheCleanTimer = null;
function logRcloneMoveError(context, error) {
if (!error) return;
@@ -764,10 +773,11 @@ function logRcloneMoveError(context, error) {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
-async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) {
+async function waitForFileStable(filePath, attempts = 8, intervalMs = 3000) {
if (!filePath || !fs.existsSync(filePath)) return false;
let prevSize = null;
let prevMtime = null;
+ let stableCount = 0;
for (let i = 0; i < attempts; i += 1) {
let stat;
try {
@@ -778,7 +788,12 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) {
const size = stat.size;
const mtime = stat.mtimeMs;
if (prevSize !== null && prevMtime !== null) {
- if (size === prevSize && mtime === prevMtime) return true;
+ if (size === prevSize && mtime === prevMtime) {
+ stableCount += 1;
+ if (stableCount >= 2) return true;
+ } else {
+ stableCount = 0;
+ }
}
prevSize = size;
prevMtime = mtime;
@@ -787,6 +802,127 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) {
return false;
}
+function computeRootVideoBytes(rootFolder) {
+ try {
+ const entries = enumerateVideoFiles(rootFolder) || [];
+ const total = entries.reduce((sum, item) => sum + (Number(item.size) || 0), 0);
+ return total || null;
+ } catch {
+ return null;
+ }
+}
+
+let rcloneStatsTimer = null;
+async function fetchRcloneStats() {
+ if (!RCLONE_RC_ENABLED) return null;
+ try {
+ let resp = await fetch(`http://${RCLONE_RC_ADDR}/core/stats`, {
+ method: "POST"
+ });
+ if (resp.status === 404) {
+ resp = await fetch(`http://${RCLONE_RC_ADDR}/rc/core/stats`, {
+ method: "POST"
+ });
+ }
+ if (!resp.ok) return null;
+ return await resp.json();
+ } catch {
+ return null;
+ }
+}
+
+function updateMoveProgressFromStats(stats) {
+ if (!stats) return false;
+ const transfers = Array.isArray(stats.transferring) ? stats.transferring : [];
+ let updated = false;
+ const applyProgress = (entry, prefix) => {
+ if (!entry) return;
+ const matched = transfers.filter((t) => String(t.name || "").includes(prefix));
+ if (matched.length) {
+ const bytes = matched.reduce((sum, t) => sum + (Number(t.bytes) || 0), 0);
+ const pct = matched.reduce((sum, t) => sum + (Number(t.percentage) || 0), 0) / matched.length;
+ if (!entry.moveTotalBytes) {
+ const totalFromStats = matched.reduce((sum, t) => sum + (Number(t.size) || 0), 0);
+ entry.moveTotalBytes = totalFromStats || entry.moveTotalBytes || null;
+ }
+ const progress = Number.isFinite(pct)
+ ? Math.min(Math.max(pct / 100, 0), 0.99)
+ : entry.moveTotalBytes
+ ? Math.min(Math.max(bytes / entry.moveTotalBytes, 0), 0.99)
+ : 0;
+ if (Number.isFinite(progress) && progress !== entry.moveProgress) {
+ entry.moveProgress = progress;
+ updated = true;
+ }
+ if (entry.moveStatus !== "uploading") {
+ entry.moveStatus = "uploading";
+ updated = true;
+ }
+ } else {
+ // Transfer görünmüyorsa queued kalır; done kararı aşağıda verilecek.
+ if (entry.moveStatus === "uploading") {
+ entry.moveStatus = "queued";
+ updated = true;
+ }
+ }
+ };
+
+ for (const entry of torrents.values()) {
+ applyProgress(entry, `${RCLONE_REMOTE_PATH}/${entry.rootFolder || ""}`);
+ }
+ for (const job of youtubeJobs.values()) {
+ applyProgress(job, `${RCLONE_REMOTE_PATH}/${job.folderId || ""}`);
+ }
+ for (const job of mailruJobs.values()) {
+ const folderPrefix = job.folderId ? `${RCLONE_REMOTE_PATH}/${job.folderId}` : null;
+ if (folderPrefix) {
+ applyProgress(job, folderPrefix);
+ }
+ }
+
+ const hasTransfers = transfers.length > 0;
+ if (!hasTransfers) {
+ const markDoneIfReady = (entry) => {
+ if (!entry || !entry.moveTotalBytes) return;
+ if (entry.moveStatus === "queued" || entry.moveStatus === "uploading") {
+ if ((entry.moveProgress || 0) >= 0.99) {
+ entry.moveStatus = "done";
+ entry.moveProgress = 1;
+ updated = true;
+ }
+ }
+ };
+ for (const entry of torrents.values()) markDoneIfReady(entry);
+ for (const job of youtubeJobs.values()) markDoneIfReady(job);
+ for (const job of mailruJobs.values()) markDoneIfReady(job);
+ }
+ return updated;
+}
+
+function startRcloneStatsPolling() {
+ if (rcloneStatsTimer) return;
+ rcloneStatsTimer = setInterval(async () => {
+ const hasActive = Array.from(torrents.values()).some((e) =>
+ ["queued", "uploading"].includes(e.moveStatus)
+ ) ||
+ Array.from(youtubeJobs.values()).some((e) =>
+ ["queued", "uploading"].includes(e.moveStatus)
+ ) ||
+ Array.from(mailruJobs.values()).some((e) =>
+ ["queued", "uploading"].includes(e.moveStatus)
+ );
+ if (!hasActive) {
+ clearInterval(rcloneStatsTimer);
+ rcloneStatsTimer = null;
+ return;
+ }
+ const stats = await fetchRcloneStats();
+ if (updateMoveProgressFromStats(stats)) {
+ scheduleSnapshotBroadcast();
+ }
+ }, 2000);
+}
+
function parseRcloneTokenFromText(text) {
if (!text || !text.includes("access_token")) return null;
const start = text.indexOf("{");
@@ -816,6 +952,7 @@ function loadRcloneSettings() {
return {
autoMove: false,
autoMount: false,
+ cacheCleanMinutes: 0,
remoteName: RCLONE_REMOTE_NAME,
remotePath: RCLONE_REMOTE_PATH,
mountDir: RCLONE_MOUNT_DIR,
@@ -827,6 +964,7 @@ function loadRcloneSettings() {
return {
autoMove: Boolean(data.autoMove),
autoMount: Boolean(data.autoMount),
+ cacheCleanMinutes: Number(data.cacheCleanMinutes) || 0,
remoteName: data.remoteName || RCLONE_REMOTE_NAME,
remotePath: data.remotePath || RCLONE_REMOTE_PATH,
mountDir: data.mountDir || RCLONE_MOUNT_DIR,
@@ -837,6 +975,7 @@ function loadRcloneSettings() {
return {
autoMove: false,
autoMount: false,
+ cacheCleanMinutes: 0,
remoteName: RCLONE_REMOTE_NAME,
remotePath: RCLONE_REMOTE_PATH,
mountDir: RCLONE_MOUNT_DIR,
@@ -856,6 +995,55 @@ function saveRcloneSettings(partial) {
return next;
}
+function startRcloneCacheCleanSchedule(minutes) {
+ if (rcloneCacheCleanTimer) {
+ clearInterval(rcloneCacheCleanTimer);
+ rcloneCacheCleanTimer = null;
+ }
+ const interval = Number(minutes);
+ if (!interval || interval <= 0) return;
+ rcloneCacheCleanTimer = setInterval(() => {
+ if (!RCLONE_RC_ENABLED) return;
+ fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { method: "POST" })
+ .then((resp) => {
+ if (resp.status === 404) {
+ return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, {
+ method: "POST"
+ });
+ }
+ return resp;
+ })
+ .then(() => {
+ console.log("🧹 Rclone cache temizleme tetiklendi.");
+ })
+ .catch(() => {
+ console.warn("⚠️ Rclone cache temizleme başarısız.");
+ });
+ }, interval * 60 * 1000);
+}
+
+async function runRcloneCacheClean() {
+ const settings = loadRcloneSettings();
+ const wasRunning = Boolean(rcloneProcess && !rcloneProcess.killed);
+ if (wasRunning) {
+ stopRcloneMount();
+ }
+ try {
+ fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true });
+ fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true });
+ if (wasRunning) {
+ const result = startRcloneMount(settings);
+ if (!result.ok) {
+ return { ok: false, error: result.error || "Rclone yeniden başlatılamadı" };
+ }
+ return { ok: true, method: "fs", restarted: true };
+ }
+ return { ok: true, method: "fs", restarted: false };
+ } catch (err) {
+ return { ok: false, error: err?.message || String(err) };
+ }
+}
+
function isRcloneMounted(mountDir) {
if (!mountDir) return false;
try {
@@ -903,6 +1091,10 @@ function startRcloneMount(settings) {
RCLONE_VFS_CACHE_MODE,
"--cache-dir",
RCLONE_VFS_CACHE_DIR,
+ "--vfs-cache-max-size",
+ RCLONE_VFS_CACHE_MAX_SIZE,
+ "--vfs-cache-max-age",
+ RCLONE_VFS_CACHE_MAX_AGE,
"--dir-cache-time",
RCLONE_DIR_CACHE_TIME,
"--poll-interval",
@@ -910,6 +1102,11 @@ function startRcloneMount(settings) {
"--log-level",
"INFO"
];
+ if (RCLONE_RC_ENABLED) {
+ args.push("--rc");
+ args.push("--rc-addr", RCLONE_RC_ADDR);
+ args.push("--rc-no-auth");
+ }
try {
rcloneProcess = spawn("rclone", args, {
@@ -1297,6 +1494,8 @@ function startYoutubeDownload(url, { moveToGdrive = false } = {}) {
moveToGdrive: Boolean(moveToGdrive),
moveStatus: "idle",
moveError: null,
+ moveProgress: null,
+ moveTotalBytes: null,
progress: 0,
downloaded: 0,
totalBytes: 0,
@@ -1652,17 +1851,20 @@ async function finalizeYoutubeJob(job, exitCode) {
console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`);
if (job.moveToGdrive) {
- job.moveStatus = "moving";
+ job.moveStatus = "queued";
job.moveError = null;
+ job.moveProgress = 0;
+ job.moveTotalBytes = job.totalBytes || computeRootVideoBytes(job.folderId) || null;
scheduleSnapshotBroadcast();
+ startRcloneStatsPolling();
const moveResult = await moveRootFolderToGdrive(job.folderId);
- if (moveResult.ok) {
- job.moveStatus = "done";
- } else {
- job.moveStatus = "error";
- job.moveError = moveResult.error || "GDrive taşıma hatası";
- logRcloneMoveError(`youtube:${job.id}`, job.moveError);
- }
+ if (moveResult.ok) {
+ // Upload tamamlanma durumu RC stats ile belirlenecek
+ } else {
+ job.moveStatus = "error";
+ job.moveError = moveResult.error || "GDrive taşıma hatası";
+ logRcloneMoveError(`youtube:${job.id}`, job.moveError);
+ }
broadcastFileUpdate("downloads");
scheduleSnapshotBroadcast();
}
@@ -1818,7 +2020,9 @@ function mailruSnapshot(job) {
status: job.state,
moveToGdrive: job.moveToGdrive || false,
moveStatus: job.moveStatus || "idle",
- moveError: job.moveError || null
+ moveError: job.moveError || null,
+ moveProgress: job.moveProgress ?? null,
+ moveTotalBytes: job.moveTotalBytes ?? null
};
}
@@ -1993,12 +2197,15 @@ async function finalizeMailRuJob(job, exitCode) {
console.log(`✅ Mail.ru indirmesi tamamlandı: ${job.title}`);
if (job.moveToGdrive) {
- job.moveStatus = "moving";
+ job.moveStatus = "queued";
job.moveError = null;
+ job.moveProgress = 0;
+ job.moveTotalBytes = job.totalBytes || null;
scheduleSnapshotBroadcast();
+ startRcloneStatsPolling();
const moveResult = await movePathToGdrive(relPath);
if (moveResult.ok) {
- job.moveStatus = "done";
+ // Upload tamamlanma durumu RC stats ile belirlenecek
} else {
job.moveStatus = "error";
job.moveError = moveResult.error || "GDrive taşıma hatası";
@@ -2101,6 +2308,8 @@ async function startMailRuDownload(url, { moveToGdrive = false } = {}) {
moveToGdrive: Boolean(moveToGdrive),
moveStatus: "idle",
moveError: null,
+ moveProgress: null,
+ moveTotalBytes: null,
state: "awaiting_match",
progress: 0,
downloaded: 0,
@@ -2390,7 +2599,9 @@ function youtubeSnapshot(job) {
status: job.state,
moveToGdrive: job.moveToGdrive || false,
moveStatus: job.moveStatus || "idle",
- moveError: job.moveError || null
+ moveError: job.moveError || null,
+ moveProgress: job.moveProgress ?? null,
+ moveTotalBytes: job.moveTotalBytes ?? null
};
}
@@ -6471,7 +6682,9 @@ function snapshot() {
thumbnail: entry?.thumbnail || null,
moveToGdrive: entry?.moveToGdrive || false,
moveStatus: entry?.moveStatus || "idle",
- moveError: entry?.moveError || null
+ moveError: entry?.moveError || null,
+ moveProgress: entry?.moveProgress ?? null,
+ moveTotalBytes: entry?.moveTotalBytes ?? null
};
}
);
@@ -6507,7 +6720,9 @@ function wireTorrent(
rootFolder: savePath ? path.basename(savePath) : null,
moveToGdrive: Boolean(moveToGdrive),
moveStatus: "idle",
- moveError: null
+ moveError: null,
+ moveProgress: null,
+ moveTotalBytes: null
});
const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast();
@@ -6820,12 +7035,23 @@ async function onTorrentDone({ torrent }) {
}
if (entry.moveToGdrive) {
- entry.moveStatus = "moving";
+ const paused = pauseTorrentEntry(entry);
+ if (paused) {
+ console.log(`⏸️ GDrive taşıma için torrent durduruldu: ${entry.infoHash}`);
+ }
+ entry.moveStatus = "queued";
entry.moveError = null;
+ entry.moveProgress = 0;
+ entry.moveTotalBytes =
+ entry.totalBytes ||
+ torrent?.length ||
+ computeRootVideoBytes(rootFolder) ||
+ null;
scheduleSnapshotBroadcast();
+ startRcloneStatsPolling();
const moveResult = await moveRootFolderToGdrive(rootFolder);
if (moveResult.ok) {
- entry.moveStatus = "done";
+ // Upload tamamlanma durumu RC stats ile belirlenecek
} else {
entry.moveStatus = "error";
entry.moveError = moveResult.error || "GDrive taşıma hatası";
@@ -8771,6 +8997,7 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
configPath: settings.configPath,
autoMove: settings.autoMove,
autoMount: settings.autoMount,
+ cacheCleanMinutes: settings.cacheCleanMinutes || 0,
configExists: fs.existsSync(settings.configPath),
remoteConfigured: rcloneConfigHasRemote(settings.remoteName),
lastError: rcloneLastError || null
@@ -8779,15 +9006,17 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
app.post("/api/rclone/settings", requireAuth, (req, res) => {
try {
- const { autoMove, autoMount, remoteName, remotePath, mountDir } = req.body || {};
+ const { autoMove, autoMount, remoteName, remotePath, mountDir, cacheCleanMinutes } = req.body || {};
const next = saveRcloneSettings({
autoMove: Boolean(autoMove),
autoMount: Boolean(autoMount),
+ cacheCleanMinutes: Number(cacheCleanMinutes) || 0,
remoteName: remoteName || RCLONE_REMOTE_NAME,
remotePath: remotePath || RCLONE_REMOTE_PATH,
mountDir: mountDir || RCLONE_MOUNT_DIR,
configPath: RCLONE_CONFIG_PATH
});
+ startRcloneCacheCleanSchedule(next.cacheCleanMinutes);
res.json({ ok: true, settings: next });
} catch (err) {
res.status(500).json({ ok: false, error: err?.message || String(err) });
@@ -8905,6 +9134,48 @@ app.post("/api/rclone/mount", requireAuth, (req, res) => {
return res.json({ ok: true, ...result });
});
+app.post("/api/rclone/cache/clean", requireAuth, async (req, res) => {
+ const result = await runRcloneCacheClean();
+ if (!result.ok) {
+ return res.status(500).json({ ok: false, error: result.error });
+ }
+ return res.json({ ok: true, ...result });
+});
+
+app.get("/api/rclone/conf", requireAuth, (req, res) => {
+ try {
+ if (!fs.existsSync(RCLONE_CONFIG_PATH)) {
+ return res.json({ ok: true, content: "" });
+ }
+ const content = fs.readFileSync(RCLONE_CONFIG_PATH, "utf-8");
+ res.json({ ok: true, content });
+ } catch (err) {
+ res.status(500).json({ ok: false, error: err?.message || String(err) });
+ }
+});
+
+app.post("/api/rclone/conf", requireAuth, (req, res) => {
+ try {
+ const content = String(req.body?.content || "");
+ if (!content.trim()) {
+ return res.status(400).json({ ok: false, error: "Boş içerik gönderilemez." });
+ }
+ ensureDirForFile(RCLONE_CONFIG_PATH);
+ fs.writeFileSync(RCLONE_CONFIG_PATH, content, "utf-8");
+ const settings = loadRcloneSettings();
+ if (rcloneProcess && !rcloneProcess.killed) {
+ stopRcloneMount();
+ const restart = startRcloneMount(settings);
+ if (!restart.ok) {
+ return res.status(500).json({ ok: false, error: restart.error || "Rclone yeniden başlatılamadı." });
+ }
+ }
+ res.json({ ok: true });
+ } catch (err) {
+ res.status(500).json({ ok: false, error: err?.message || String(err) });
+ }
+});
+
app.post("/api/rclone/unmount", requireAuth, (req, res) => {
const result = stopRcloneMount();
if (!result.ok) {
@@ -10180,6 +10451,7 @@ if (RCLONE_ENABLED && initialRcloneSettings.autoMount) {
console.warn(`⚠️ Rclone mount başlatılamadı: ${result.error}`);
}
}
+startRcloneCacheCleanSchedule(initialRcloneSettings.cacheCleanMinutes || 0);
// --- ✅ Client build (frontend) dosyalarını sun ---
const publicDir = path.join(__dirname, "public");