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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
310
server/server.js
310
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");
|
||||
|
||||
Reference in New Issue
Block a user