feat(rclone): RC API ilerleme takibi ve conf editörü ekle

- Rclone RC API kullanılarak dosya yüklemelerinde anlık ilerleme çubuğu eklendi.
- Arayüz üzerinden `rclone.conf` dosyası düzenlenebilir hale getirildi.
- VFS cache boyutu/yaş sınırları ve otomatik temizleme ayarı eklendi.
- Manuel yetkilendirme alanları kaldırıldı.
This commit is contained in:
2026-02-02 15:26:16 +03:00
parent 0fa3a818ae
commit cd4769b3c1
4 changed files with 404 additions and 132 deletions

View File

@@ -63,6 +63,12 @@ RCLONE_DIR_CACHE_TIME=1m
RCLONE_VFS_CACHE_MODE=full
# Rclone VFS cache dizini
RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs
# Rclone VFS cache sınırları
RCLONE_VFS_CACHE_MAX_SIZE=20G
RCLONE_VFS_CACHE_MAX_AGE=24h
# Rclone RC (progress) API
RCLONE_RC_ENABLED=1
RCLONE_RC_ADDR=127.0.0.1:5572
# Rclone debug log (taşıma hatalarını detaylı loglamak için)
RCLONE_DEBUG_MODE_LOG=0
# Media stream debug log (akış kaynağını loglamak için)

View File

@@ -25,15 +25,11 @@
let rcloneStatus = null;
let rcloneLoading = false;
let rcloneSaving = false;
let rcloneAuthUrl = "";
let rcloneToken = "";
let rcloneClientId = "";
let rcloneClientSecret = "";
let rcloneAutoMove = false;
let rcloneAutoMount = false;
let rcloneRemoteName = "";
let rcloneRemotePath = "";
let rcloneMountDir = "";
let rcloneCacheCleanMinutes = 0;
let rcloneConfText = "";
let rcloneConfVisible = false;
async function loadCookies() {
loadingCookies = true;
@@ -135,9 +131,7 @@
rcloneStatus = data;
rcloneAutoMove = Boolean(data?.autoMove);
rcloneAutoMount = Boolean(data?.autoMount);
rcloneRemoteName = data?.remoteName || "";
rcloneRemotePath = data?.remotePath || "";
rcloneMountDir = data?.mountDir || "";
rcloneCacheCleanMinutes = Number(data?.cacheCleanMinutes) || 0;
} catch (err) {
error = err?.message || "Rclone durumu alınamadı.";
} finally {
@@ -145,6 +139,19 @@
}
}
async function loadRcloneConf() {
try {
const resp = await apiFetch("/api/rclone/conf");
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
rcloneConfText = data.content || "";
} catch (err) {
error = err?.message || "rclone.conf okunamadı.";
}
}
async function saveRcloneSettings() {
if (rcloneSaving) return;
rcloneSaving = true;
@@ -157,15 +164,22 @@
body: JSON.stringify({
autoMove: rcloneAutoMove,
autoMount: rcloneAutoMount,
remoteName: rcloneRemoteName,
remotePath: rcloneRemotePath,
mountDir: rcloneMountDir
cacheCleanMinutes: rcloneCacheCleanMinutes
})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
const confResp = await apiFetch("/api/rclone/conf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: rcloneConfText })
});
const confData = await confResp.json().catch(() => ({}));
if (!confResp.ok || !confData?.ok) {
throw new Error(confData?.error || `HTTP ${confResp.status}`);
}
success = "Rclone ayarları kaydedildi.";
await loadRcloneStatus();
} catch (err) {
@@ -175,50 +189,6 @@
}
}
async function requestRcloneAuthUrl() {
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/auth-url");
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
rcloneAuthUrl = data.url || "";
} catch (err) {
error = err?.message || "Auth URL alınamadı.";
}
}
async function applyRcloneToken() {
if (!rcloneToken) {
error = "Token zorunlu.";
return;
}
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: rcloneToken,
clientId: rcloneClientId,
clientSecret: rcloneClientSecret,
remoteName: rcloneRemoteName
})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "Token kaydedildi.";
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Token kaydedilemedi.";
}
}
async function startRcloneMount() {
error = null;
success = null;
@@ -251,10 +221,26 @@
}
}
async function cleanRcloneCache() {
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "Rclone cache temizlendi.";
} catch (err) {
error = err?.message || "Rclone cache temizlenemedi.";
}
}
onMount(() => {
loadCookies();
loadYoutubeSettings();
loadRcloneStatus();
loadRcloneConf();
});
function formatDate(ts) {
@@ -318,7 +304,7 @@
</label>
</div>
<div class="actions">
<div class="actions left">
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
@@ -407,73 +393,47 @@
<div class="field inline compact left-align">
<div class="inline-field">
<label for="rclone-remote">Remote adı</label>
<label for="rclone-cache-clean">Cache temizleme (dakika)</label>
<input
id="rclone-remote"
type="text"
bind:value={rcloneRemoteName}
disabled={rcloneLoading || rcloneSaving}
/>
</div>
<div class="inline-field">
<label for="rclone-path">Drive klasörü</label>
<input
id="rclone-path"
type="text"
bind:value={rcloneRemotePath}
id="rclone-cache-clean"
type="number"
min="0"
step="1"
bind:value={rcloneCacheCleanMinutes}
disabled={rcloneLoading || rcloneSaving}
/>
</div>
<button class="btn" on:click={cleanRcloneCache}>
<i class="fa-solid fa-broom"></i> Clean Cache
</button>
</div>
<div class="field">
<label for="rclone-mount">Mount dizini</label>
<input
id="rclone-mount"
type="text"
bind:value={rcloneMountDir}
disabled={rcloneLoading || rcloneSaving}
/>
<label>rclone.conf</label>
<div class="password-field">
<textarea
class="conf-textarea {rcloneConfVisible ? '' : 'masked'}"
bind:value={rcloneConfText}
placeholder="rclone.conf içeriğini yapıştırın"
></textarea>
<button
class="eye-btn"
type="button"
on:click={() => (rcloneConfVisible = !rcloneConfVisible)}
aria-label="Gizli/Görünür"
>
<i class={rcloneConfVisible ? "fa-solid fa-eye-slash" : "fa-solid fa-eye"}></i>
</button>
</div>
</div>
<div class="actions">
<button class="btn" on:click={loadRcloneStatus} disabled={rcloneLoading || rcloneSaving}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
<button class="btn primary" on:click={saveRcloneSettings} disabled={rcloneLoading || rcloneSaving}>
<i class="fa-solid fa-floppy-disk"></i> Kaydet
</button>
</div>
<div class="field">
<label>Yetkilendirme</label>
<div class="actions">
<button class="btn" on:click={requestRcloneAuthUrl}>
<i class="fa-solid fa-link"></i> Auth URL al
</button>
</div>
{#if rcloneAuthUrl}
<input type="text" readonly value={rcloneAuthUrl} />
{/if}
</div>
<div class="field">
<label for="rclone-client-id">Client ID (opsiyonel)</label>
<input id="rclone-client-id" type="text" bind:value={rcloneClientId} />
</div>
<div class="field">
<label for="rclone-client-secret">Client Secret (opsiyonel)</label>
<input id="rclone-client-secret" type="text" bind:value={rcloneClientSecret} />
</div>
<div class="field">
<label for="rclone-token">Token</label>
<textarea id="rclone-token" bind:value={rcloneToken} placeholder="rclone authorize çıktısındaki token JSON'u"></textarea>
</div>
<div class="actions">
<button class="btn" on:click={applyRcloneToken}>
<i class="fa-solid fa-key"></i> Token Kaydet
</button>
<button class="btn" on:click={startRcloneMount}>
<i class="fa-solid fa-play"></i> Mount Başlat
</button>
@@ -707,4 +667,32 @@
background: #e5ffe7;
color: #0f7a1f;
}
.password-field {
position: relative;
}
.conf-textarea {
width: 100%;
min-height: 180px;
resize: vertical;
font-family: "Courier New", monospace;
letter-spacing: 0.2px;
}
.conf-textarea.masked {
-webkit-text-security: disc;
text-security: disc;
}
.eye-btn {
position: absolute;
right: 20px;
top: 8px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
padding: 6px 8px;
cursor: pointer;
}
</style>

View File

@@ -855,18 +855,20 @@
</div>
{/if}
<div class="progress-bar">
<div class="progress-bar {t.moveStatus === 'uploading' ? 'uploading' : ''}">
<div
class="progress"
style="width:{(t.progress || 0) * 100}%"
style="width:{(t.moveStatus === 'uploading' ? (t.moveProgress || 0) : (t.progress || 0)) * 100}%"
></div>
</div>
<div class="progress-text">
{#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;

View File

@@ -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");