Compare commits
22 Commits
main
...
d705e37d85
| Author | SHA1 | Date | |
|---|---|---|---|
| d705e37d85 | |||
| 2f3dc72dcc | |||
| a011af7368 | |||
| 90587aa6d6 | |||
| c3d38d2e79 | |||
| 2b9c776c8a | |||
| 7269f52b0e | |||
| 1a7a8ec66e | |||
| 1b0662a5ec | |||
| 8825d0af8d | |||
| 44323275d8 | |||
| 20da34beb2 | |||
| 2b5bb86b3e | |||
| 1e4fb38cfb | |||
| c61f1b0288 | |||
| e34b8fc024 | |||
| a95c844af9 | |||
| ca88b7816a | |||
| e44c21b36a | |||
| cd4769b3c1 | |||
| 0fa3a818ae | |||
| e7aaea53ad |
56
.env.example
56
.env.example
@@ -43,3 +43,59 @@ WEBDAV_PATH=/webdav
|
||||
WEBDAV_READONLY=1
|
||||
# WebDAV index yeniden oluşturma süresi (ms).
|
||||
WEBDAV_INDEX_TTL=60000
|
||||
|
||||
# --- Rclone / Google Drive ---
|
||||
# Rclone entegrasyonunu aç/kapat
|
||||
RCLONE_ENABLED=0
|
||||
# Rclone config dosyası konumu (container içinde)
|
||||
RCLONE_CONFIG_PATH=/config/rclone/rclone.conf
|
||||
# Google Drive mount edilecek dizin (container içinde)
|
||||
RCLONE_MOUNT_DIR=/app/server/gdrive
|
||||
# Rclone remote adı
|
||||
RCLONE_REMOTE_NAME=dupe
|
||||
# Google Drive içinde kullanılacak klasör adı
|
||||
RCLONE_REMOTE_PATH=Dupe
|
||||
# Rclone mount tazeleme/poll süresi
|
||||
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
|
||||
# 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 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
|
||||
# Rclone settings dosya yolu (container içinde)
|
||||
RCLONE_SETTINGS_PATH=/app/server/cache/rclone.json
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN npm run build
|
||||
|
||||
# Build server
|
||||
FROM node:22-slim
|
||||
RUN apt-get update && apt-get install -y ffmpeg curl aria2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y ffmpeg curl aria2 rclone fuse3 && rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp \
|
||||
&& chmod a+rx /usr/local/bin/yt-dlp
|
||||
WORKDIR /app/server
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Sidebar from "./components/Sidebar.svelte";
|
||||
import Topbar from "./components/Topbar.svelte";
|
||||
import MiniPlayer from "./components/MiniPlayer.svelte";
|
||||
import Toast from "./components/Toast.svelte";
|
||||
import Files from "./routes/Files.svelte";
|
||||
import Transfers from "./routes/Transfers.svelte";
|
||||
import Trash from "./routes/Trash.svelte";
|
||||
@@ -163,6 +164,7 @@
|
||||
</div>
|
||||
|
||||
<MiniPlayer />
|
||||
<Toast />
|
||||
|
||||
<!-- Sidebar dışına tıklayınca kapanma -->
|
||||
{#if menuOpen}
|
||||
|
||||
86
client/src/components/Toast.svelte
Normal file
86
client/src/components/Toast.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import { toast } from '../stores/toastStore.js';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let toastData = {
|
||||
message: null,
|
||||
type: 'success',
|
||||
visible: false
|
||||
};
|
||||
|
||||
toast.subscribe(value => {
|
||||
toastData = value;
|
||||
});
|
||||
|
||||
const icons = {
|
||||
success: 'fa-solid fa-circle-check',
|
||||
error: 'fa-solid fa-circle-exclamation',
|
||||
info: 'fa-solid fa-circle-info'
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if toastData.visible && toastData.message}
|
||||
<div class="toast-container" transition:fade={{ duration: 200 }}>
|
||||
<div class="toast {toastData.type}">
|
||||
<i class="{icons[toastData.type] || icons.info}"></i>
|
||||
<span>{toastData.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 9999;
|
||||
max-width: min(350px, 90vw);
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.toast i {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: linear-gradient(135deg, rgba(0, 200, 83, 0.95), rgba(0, 150, 63, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: linear-gradient(135deg, rgba(220, 38, 38, 0.95), rgba(185, 28, 28, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(37, 99, 235, 0.95));
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.toast-container {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -468,6 +468,7 @@
|
||||
let pendingPlayTarget = null;
|
||||
let activeMenu = null; // Aktif menü öğesi
|
||||
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
|
||||
let deleteConfirmPending = false; // Silme onayı beklemede mi
|
||||
let showMatchModal = false;
|
||||
let matchingFile = null;
|
||||
let matchTitle = "";
|
||||
@@ -604,6 +605,27 @@
|
||||
refreshMovieCount();
|
||||
refreshTvShowCount();
|
||||
}
|
||||
|
||||
async function moveToGdrive(entry) {
|
||||
if (!entry?.name) return;
|
||||
const ok = confirm("Bu öğe GDrive'a taşınsın mı?");
|
||||
if (!ok) return;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/move", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path: entry.name })
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
alert(data?.error || "GDrive taşıma başarısız oldu.");
|
||||
return;
|
||||
}
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
alert(err?.message || "GDrive taşıma başarısız oldu.");
|
||||
}
|
||||
}
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return "0 MB";
|
||||
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
|
||||
@@ -807,12 +829,12 @@
|
||||
const sourcePath = resolveEntryOriginalPath(source);
|
||||
const targetPath = resolveEntryOriginalPath(target);
|
||||
|
||||
if (!sourcePath || !targetPath) return;
|
||||
if (!sourcePath || targetPath === undefined || targetPath === null) return;
|
||||
|
||||
const normalizedSource = normalizePath(sourcePath);
|
||||
const normalizedTarget = normalizePath(targetPath);
|
||||
const normalizedTarget = normalizePath(targetPath || "");
|
||||
|
||||
if (!normalizedSource || !normalizedTarget) return;
|
||||
if (!normalizedSource) return;
|
||||
|
||||
if (source?.isDirectory) {
|
||||
if (
|
||||
@@ -830,7 +852,8 @@
|
||||
}
|
||||
|
||||
const sourceLabel = cleanFileName(source.displayName || source.name || normalizedSource);
|
||||
const targetLabel = cleanFileName(target.displayName || target.name || normalizedTarget);
|
||||
const targetLabel =
|
||||
cleanFileName(target.displayName || target.name || normalizedTarget) || "Home";
|
||||
if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" içine taşımak istiyor musun?`)) {
|
||||
return;
|
||||
}
|
||||
@@ -1236,7 +1259,7 @@
|
||||
const button = event.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
const menuWidth = 160;
|
||||
const menuHeight = 140; // Yaklaşık menü yüksekliği
|
||||
const menuHeight = 180; // Yaklaşık menü yüksekliği
|
||||
|
||||
// Üç noktanın son noktası ile menünün sol kenarını hizala
|
||||
// Düğme genişliği 34px, son nokta sağ kenara yakın
|
||||
@@ -1261,10 +1284,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
activeMenu = null;
|
||||
}
|
||||
|
||||
async function downloadFile(file) {
|
||||
if (!file || file.isDirectory) {
|
||||
if (file?.isDirectory) navigateToPath(file.displayPath);
|
||||
@@ -1422,48 +1441,53 @@
|
||||
|
||||
async function deleteFile(item) {
|
||||
if (!item) return;
|
||||
const target = resolveDeletionTargets(item);
|
||||
if (!target) {
|
||||
|
||||
// Eğer onay beklemedeyse, silme işlemini gerçekleştir
|
||||
if (deleteConfirmPending) {
|
||||
const target = resolveDeletionTargets(item);
|
||||
if (!target) {
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await performDeletion(target);
|
||||
deleteConfirmPending = false; // Reset flag
|
||||
|
||||
if (!result.ok) {
|
||||
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isDirectory) {
|
||||
const displayKey = normalizePath(
|
||||
item.displayPath ||
|
||||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
|
||||
);
|
||||
if (displayKey || displayKey === "") {
|
||||
pendingFolders.delete(displayKey);
|
||||
}
|
||||
}
|
||||
|
||||
await loadFiles();
|
||||
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
|
||||
selectedItems = new Set(
|
||||
[...selectedItems].filter((name) => name !== item.name),
|
||||
);
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const label =
|
||||
target.type === "directory"
|
||||
? target.label || item.displayName || "Klasör"
|
||||
: target.label || cleanFileName(item.name);
|
||||
const message =
|
||||
target.type === "directory"
|
||||
? `"${label}" klasörünü silmek istediğine emin misin?`
|
||||
: `"${label}" dosyasını silmek istediğinizden emin misiniz?`;
|
||||
|
||||
if (!confirm(message)) {
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await performDeletion(target);
|
||||
if (!result.ok) {
|
||||
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
if (item.isDirectory) {
|
||||
const displayKey = normalizePath(
|
||||
item.displayPath ||
|
||||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
|
||||
);
|
||||
if (displayKey || displayKey === "") {
|
||||
pendingFolders.delete(displayKey);
|
||||
}
|
||||
// İlk tıklama - onay moduna geç
|
||||
deleteConfirmPending = true;
|
||||
}
|
||||
|
||||
await loadFiles();
|
||||
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
|
||||
selectedItems = new Set(
|
||||
[...selectedItems].filter((name) => name !== item.name),
|
||||
);
|
||||
closeMenu();
|
||||
// Menü kapandığında onay durumunu resetle
|
||||
function closeMenu() {
|
||||
activeMenu = null;
|
||||
deleteConfirmPending = false;
|
||||
showMatchModal = false;
|
||||
matchingFile = null;
|
||||
}
|
||||
|
||||
// Klasör oluşturma fonksiyonları
|
||||
@@ -1644,15 +1668,15 @@
|
||||
const sourcePath = resolveEntryOriginalPath(clipboardItem);
|
||||
const targetPath = resolveOriginalPathForDisplay(currentPath, currentOriginalPath);
|
||||
|
||||
if (!sourcePath || !targetPath) {
|
||||
if (!sourcePath || targetPath === undefined || targetPath === null) {
|
||||
alert("Geçersiz kaynak veya hedef yolu");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSource = normalizePath(sourcePath);
|
||||
const normalizedTarget = normalizePath(targetPath);
|
||||
const normalizedTarget = normalizePath(targetPath || "");
|
||||
|
||||
if (!normalizedSource || !normalizedTarget) {
|
||||
if (!normalizedSource) {
|
||||
alert("Geçersiz kaynak veya hedef yolu");
|
||||
return;
|
||||
}
|
||||
@@ -1665,7 +1689,7 @@
|
||||
}
|
||||
|
||||
const sourceLabel = cleanFileName(clipboardItem.displayName || clipboardItem.name || normalizedSource);
|
||||
const targetLabel = cleanFileName(currentPath || normalizedTarget);
|
||||
const targetLabel = cleanFileName(currentPath || normalizedTarget) || "Home";
|
||||
const actionLabel = clipboardOperation === 'cut' ? 'taşı' : 'kopyala';
|
||||
if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" konumuna ${actionLabel}mak istiyor musun?`)) {
|
||||
return;
|
||||
@@ -2389,11 +2413,19 @@
|
||||
<div class="menu-divider"></div>
|
||||
{/if}
|
||||
<button
|
||||
class="menu-item delete"
|
||||
class="menu-item"
|
||||
on:click|stopPropagation={() => moveToGdrive(activeMenu)}
|
||||
>
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>GDrive'a Taşı</span>
|
||||
</button>
|
||||
<div class="menu-divider"></div>
|
||||
<button
|
||||
class="menu-item delete {deleteConfirmPending ? 'confirming' : ''}"
|
||||
on:click|stopPropagation={() => deleteFile(activeMenu)}
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span>Sil</span>
|
||||
<span>{deleteConfirmPending ? 'Emin misiniz?' : 'Sil'}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -3949,7 +3981,17 @@
|
||||
.menu-item.delete {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
|
||||
.menu-item.delete.confirming {
|
||||
color: #fff;
|
||||
background-color: #e53935;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-item.delete.confirming:hover {
|
||||
background-color: #c62828;
|
||||
}
|
||||
|
||||
.menu-item.delete:hover {
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { apiFetch } from "../utils/api.js";
|
||||
import { showToast } from "../stores/toastStore.js";
|
||||
|
||||
const tabs = [
|
||||
{ id: "general", label: "General", icon: "fa-solid fa-sliders" },
|
||||
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" },
|
||||
{ id: "rclone", label: "Rclone", icon: "fa-solid fa-cloud" },
|
||||
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
|
||||
];
|
||||
|
||||
@@ -19,12 +21,19 @@
|
||||
let onlyAudio = false;
|
||||
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
|
||||
let error = null;
|
||||
let success = null;
|
||||
|
||||
let rcloneStatus = null;
|
||||
let rcloneLoading = false;
|
||||
let rcloneSaving = false;
|
||||
let rcloneAutoMove = false;
|
||||
let rcloneAutoMount = false;
|
||||
let rcloneCacheCleanMinutes = 0;
|
||||
let rcloneConfText = "";
|
||||
let rcloneConfVisible = false;
|
||||
|
||||
async function loadCookies() {
|
||||
loadingCookies = true;
|
||||
error = null;
|
||||
success = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/youtube/cookies");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
@@ -41,7 +50,6 @@
|
||||
async function saveCookies() {
|
||||
if (savingCookies) return;
|
||||
error = null;
|
||||
success = null;
|
||||
savingCookies = true;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -61,7 +69,7 @@
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
cookiesUpdatedAt = data.updatedAt || Date.now();
|
||||
success = "Cookies kaydedildi.";
|
||||
showToast("Cookies kaydedildi.", "success");
|
||||
} catch (err) {
|
||||
error = err?.message || "Cookies kaydedilemedi.";
|
||||
} finally {
|
||||
@@ -89,7 +97,6 @@
|
||||
if (savingYtSettings) return;
|
||||
savingYtSettings = true;
|
||||
error = null;
|
||||
success = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/youtube/settings", {
|
||||
method: "POST",
|
||||
@@ -103,7 +110,7 @@
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
success = "YouTube indirme ayarları kaydedildi.";
|
||||
showToast("YouTube indirme ayarları kaydedildi.", "success");
|
||||
} catch (err) {
|
||||
error = err?.message || "YouTube ayarları kaydedilemedi.";
|
||||
} finally {
|
||||
@@ -111,9 +118,139 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRcloneStatus() {
|
||||
rcloneLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/status");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
rcloneStatus = data;
|
||||
rcloneAutoMove = Boolean(data?.autoMove);
|
||||
rcloneAutoMount = Boolean(data?.autoMount);
|
||||
rcloneCacheCleanMinutes = Number(data?.cacheCleanMinutes) || 0;
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone durumu alınamadı.";
|
||||
} finally {
|
||||
rcloneLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
autoMove: rcloneAutoMove,
|
||||
autoMount: rcloneAutoMount,
|
||||
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}`);
|
||||
}
|
||||
showToast("Rclone ayarları kaydedildi.", "success");
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone ayarları kaydedilemedi.";
|
||||
} finally {
|
||||
rcloneSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startRcloneMount() {
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/mount", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
|
||||
showToast("Rclone mount başlatılıyor...", "info");
|
||||
|
||||
// 2 saniye sonra status güncelle
|
||||
setTimeout(async () => {
|
||||
await loadRcloneStatus();
|
||||
// Status yüklendikten sonra mesajı güncelle
|
||||
if (rcloneStatus?.mounted) {
|
||||
showToast("Rclone mount başarıyla başlatıldı.", "success");
|
||||
} else if (rcloneStatus?.running) {
|
||||
showToast("Rclone mount başlatıldı, mount tamamlanıyor...", "info");
|
||||
} else {
|
||||
error = "Rclone mount başlatılamadı.";
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// İlk status güncellemesi
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone mount başlatılamadı.";
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRcloneMount() {
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/unmount", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
showToast("Rclone mount durduruldu.", "success");
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone mount durdurulamadı.";
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanRcloneCache() {
|
||||
error = 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}`);
|
||||
}
|
||||
showToast("Cache temizlendi.", "success");
|
||||
} catch (err) {
|
||||
error = err?.message || "Cache temizlenemedi.";
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadCookies();
|
||||
loadYoutubeSettings();
|
||||
loadRcloneStatus();
|
||||
loadRcloneConf();
|
||||
});
|
||||
|
||||
function formatDate(ts) {
|
||||
@@ -177,7 +314,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>
|
||||
@@ -227,15 +364,152 @@
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success}
|
||||
<div class="alert success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
{success}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === "general"}
|
||||
<div class="card muted">Genel ayarlar burada yer alacak.</div>
|
||||
{:else if activeTab === "rclone"}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-cloud"></i>
|
||||
<span>Google Drive (Rclone)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field inline compact left-align">
|
||||
<label class="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rcloneAutoMove}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
<span>İndirince otomatik taşı</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rcloneAutoMount}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
<span>Başlangıçta otomatik mount</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field inline compact left-align">
|
||||
<div class="inline-field">
|
||||
<label for="rclone-cache-clean">Cache temizleme (dakika)</label>
|
||||
<input
|
||||
id="rclone-cache-clean"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
bind:value={rcloneCacheCleanMinutes}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
</div>
|
||||
<button class="btn primary" on:click={cleanRcloneCache}>
|
||||
<i class="fa-solid fa-broom"></i> Cache Temizle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<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 primary" on:click={saveRcloneSettings} disabled={rcloneLoading || rcloneSaving}>
|
||||
<i class="fa-solid fa-floppy-disk"></i> Kaydet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert error" style="margin-top:10px;">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Mount Kontrol Kartı -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-cloud"></i>
|
||||
<span>Mount Kontrol</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions left">
|
||||
<button class="btn" on:click={startRcloneMount}>
|
||||
<i class="fa-solid fa-play"></i> Mount Başlat
|
||||
</button>
|
||||
<button class="btn" on:click={stopRcloneMount}>
|
||||
<i class="fa-solid fa-stop"></i> Mount Durdur
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if rcloneStatus}
|
||||
<div class="card muted" style="margin-top:12px;">
|
||||
<div><strong>Durum:</strong></div>
|
||||
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
|
||||
<div>
|
||||
Mounted:
|
||||
{#if rcloneStatus.mountStatus === "starting"}
|
||||
<span style="color: #f57c00;">Başlatılıyor...</span>
|
||||
{:else if rcloneStatus.mounted}
|
||||
<span style="color: #388e3c;">Evet</span>
|
||||
{:else}
|
||||
Hayır
|
||||
{/if}
|
||||
</div>
|
||||
<div>Running: {rcloneStatus.running ? "Evet" : "Hayır"}</div>
|
||||
<div>Remote: {rcloneStatus.remoteConfigured ? "Hazır" : "Eksik"}</div>
|
||||
{#if rcloneStatus.vfsCacheMode}
|
||||
<div>VFS Cache Mode: <code>{rcloneStatus.vfsCacheMode}</code></div>
|
||||
{/if}
|
||||
{#if rcloneStatus.diskUsage}
|
||||
<div style="margin-top: 8px;">
|
||||
<div style="font-size: 12px; color: #666;">Disk Kullanımı:</div>
|
||||
<div style="font-size: 13px;">
|
||||
Kullanım: %{rcloneStatus.diskUsage.usedPercent} |
|
||||
Boş: {rcloneStatus.diskUsage.availableGB.toFixed(1)}GB /
|
||||
{rcloneStatus.diskUsage.totalGB.toFixed(1)}GB
|
||||
</div>
|
||||
{#if rcloneStatus.cacheCleanThreshold}
|
||||
<div style="font-size: 11px; color: #888;">
|
||||
Temizleme eşiği: %{rcloneStatus.cacheCleanThreshold}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if rcloneStatus.lastError}
|
||||
<div style="margin-top: 8px; color: #d32f2f;">
|
||||
<i class="fa-solid fa-circle-exclamation"></i> Son hata: {rcloneStatus.lastError}
|
||||
</div>
|
||||
{/if}
|
||||
{#if rcloneStatus.lastLog && !rcloneStatus.lastError}
|
||||
<div style="margin-top: 8px; font-size: 11px; color: #666;">
|
||||
<i class="fa-solid fa-circle-info"></i> Son log: {rcloneStatus.lastLog}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === "advanced"}
|
||||
<div class="card muted">Gelişmiş ayarlar burada yer alacak.</div>
|
||||
{/if}
|
||||
@@ -437,4 +711,40 @@
|
||||
background: #e5ffe7;
|
||||
color: #0f7a1f;
|
||||
}
|
||||
|
||||
:global(code) {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
let totalDownloaded = 0;
|
||||
let totalDownloadSpeed = 0;
|
||||
let pollTimer;
|
||||
let moveToGdriveDefault = false;
|
||||
let loadingRcloneStatus = false;
|
||||
|
||||
// Modal / player state
|
||||
let showModal = false;
|
||||
@@ -79,11 +81,28 @@
|
||||
updateTransferStats();
|
||||
}
|
||||
|
||||
async function loadRcloneStatus() {
|
||||
loadingRcloneStatus = true;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/status");
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (typeof data?.autoMove === "boolean") {
|
||||
moveToGdriveDefault = data.autoMove;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Rclone durumu alınamadı:", err);
|
||||
} finally {
|
||||
loadingRcloneStatus = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(e) {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
const fd = new FormData();
|
||||
fd.append("torrent", f);
|
||||
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
|
||||
await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅
|
||||
await list();
|
||||
}
|
||||
@@ -132,7 +151,7 @@
|
||||
await apiFetch("/api/transfer", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ magnet: input })
|
||||
body: JSON.stringify({ magnet: input, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
await list();
|
||||
return;
|
||||
@@ -142,7 +161,7 @@
|
||||
const resp = await apiFetch("/api/youtube/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: normalizedYoutube })
|
||||
body: JSON.stringify({ url: normalizedYoutube, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
@@ -157,7 +176,7 @@
|
||||
const resp = await apiFetch("/api/mailru/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: normalizedMailRu })
|
||||
body: JSON.stringify({ url: normalizedMailRu, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
@@ -369,6 +388,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTransferGdrive(infoHash, enabled) {
|
||||
try {
|
||||
await apiFetch(`/api/transfer/${infoHash}/gdrive`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: enabled ? "1" : "0" })
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("GDrive toggle hatası:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function streamURL(hash, index = 0) {
|
||||
const base = `${API}/stream/${hash}?index=${index}`;
|
||||
return withToken(base);
|
||||
@@ -581,6 +612,7 @@
|
||||
for (const file of torrentsToUpload) {
|
||||
const fd = new FormData();
|
||||
fd.append("torrent", file);
|
||||
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
|
||||
await apiFetch("/api/transfer", { method: "POST", body: fd });
|
||||
}
|
||||
|
||||
@@ -603,6 +635,7 @@
|
||||
onMount(() => {
|
||||
list(); // 🔒 token'lı liste çekimi
|
||||
wsConnect(); // 🔒 token'lı WebSocket
|
||||
loadRcloneStatus();
|
||||
addGlobalDragListeners();
|
||||
const slider = document.querySelector(".volume-slider");
|
||||
if (slider) {
|
||||
@@ -642,6 +675,14 @@
|
||||
<label class="btn-primary" on:click={handleUrlInput}>
|
||||
<i class="fa-solid fa-magnet btn-icon"></i> ADD URL
|
||||
</label>
|
||||
<label class="gdrive-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={moveToGdriveDefault}
|
||||
disabled={loadingRcloneStatus}
|
||||
/>
|
||||
<span>GDrive'a taşı</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;" title="Total Transfer Speed">
|
||||
<div class="transfer-info-box">
|
||||
@@ -734,6 +775,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div style="display:flex; gap:5px;">
|
||||
<label class="gdrive-toggle compact" on:click|stopPropagation>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={t.moveToGdrive}
|
||||
on:change={(e) =>
|
||||
toggleTransferGdrive(t.infoHash, e.target.checked)}
|
||||
/>
|
||||
<span>GDrive</span>
|
||||
</label>
|
||||
{#if t.type === "torrent" || !t.type}
|
||||
<button
|
||||
class="toggle-btn"
|
||||
@@ -805,16 +855,24 @@
|
||||
</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.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"}
|
||||
GDrive Hata • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
|
||||
{:else if (t.progress || 0) < 1}
|
||||
{(t.progress * 100).toFixed(1)}% •
|
||||
{t.downloaded ? (t.downloaded / 1e6).toFixed(1) : 0} MB •
|
||||
@@ -1179,6 +1237,10 @@
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-bar.uploading .progress {
|
||||
background: linear-gradient(90deg, #ef4444, #b91c1c);
|
||||
}
|
||||
|
||||
.torrent-error {
|
||||
color: #e74c3c;
|
||||
font-size: 12px;
|
||||
@@ -1502,4 +1564,26 @@
|
||||
.more-item:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.gdrive-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #444;
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.gdrive-toggle.compact {
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gdrive-toggle input {
|
||||
accent-color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
|
||||
24
client/src/stores/toastStore.js
Normal file
24
client/src/stores/toastStore.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const toast = writable({
|
||||
message: null,
|
||||
type: 'success', // success, error, info
|
||||
visible: false
|
||||
});
|
||||
|
||||
let toastTimeout = null;
|
||||
|
||||
export function showToast(message, type = 'success', duration = 3000) {
|
||||
// Önceki toast'ı temizle
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout);
|
||||
}
|
||||
|
||||
// Yeni toast'ı göster
|
||||
toast.update({ message, type, visible: true });
|
||||
|
||||
// Belirli süre sonra gizle
|
||||
toastTimeout = setTimeout(() => {
|
||||
toast.update({ message: null, type: 'success', visible: false });
|
||||
}, duration);
|
||||
}
|
||||
@@ -7,7 +7,14 @@ services:
|
||||
volumes:
|
||||
- ./downloads:/app/server/downloads
|
||||
- ./cache:/app/server/cache
|
||||
- ./rclone:/config/rclone
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
# Login credentials for basic auth
|
||||
environment:
|
||||
USERNAME: ${USERNAME}
|
||||
@@ -25,3 +32,28 @@ services:
|
||||
WEBDAV_PATH: ${WEBDAV_PATH}
|
||||
WEBDAV_READONLY: ${WEBDAV_READONLY}
|
||||
WEBDAV_INDEX_TTL: ${WEBDAV_INDEX_TTL}
|
||||
RCLONE_ENABLED: ${RCLONE_ENABLED}
|
||||
RCLONE_CONFIG_PATH: ${RCLONE_CONFIG_PATH}
|
||||
RCLONE_MOUNT_DIR: ${RCLONE_MOUNT_DIR}
|
||||
RCLONE_REMOTE_NAME: ${RCLONE_REMOTE_NAME}
|
||||
RCLONE_REMOTE_PATH: ${RCLONE_REMOTE_PATH}
|
||||
RCLONE_POLL_INTERVAL: ${RCLONE_POLL_INTERVAL}
|
||||
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}
|
||||
RCLONE_SETTINGS_PATH: ${RCLONE_SETTINGS_PATH}
|
||||
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "dupe",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
1694
server/server.js
1694
server/server.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user