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

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