Compare commits

...

22 Commits

Author SHA1 Message Date
d705e37d85 feat(ui): toast bildirim sistemini ekle
Toast bileşeni ve store oluşturuldu. Ayarlar sayfasındaki işlem bildirimleri yeni sistemle gösterilmeye başlandı.
2026-02-03 12:34:49 +03:00
2f3dc72dcc chore(rclone): mailru transfer eşleşme debug logu ekle
MailRu yüklemelerinde transfer eşleşmesi bulunamadığında sorunun
teşhisi için job ve transfer detaylarını içeren uyarı logu eklendi.
2026-02-03 12:03:48 +03:00
a011af7368 fix(rclone): mailru için fileName fallback ekle 2026-02-03 11:53:47 +03:00
90587aa6d6 fix(rclone): avatar dosya yolunu düzelt 2026-02-03 11:33:27 +03:00
c3d38d2e79 fix(rclone): aktarım durum takibini düzelt
Dosya sistemi tabanlı tamamlanma kontrolünü kaldırır.
Aktarım listesinde olmayan "uploading" durumundaki öğeleri "queued"ye
çevirir. Bu sayede polling süreci devam eder ve tamamlanma kararı
aktarım listesinin boşalmasına bırakılır.
2026-02-03 11:23:30 +03:00
2b9c776c8a fix(rclone): taşıma tamamlanma koşullarını düzelt
Taşıma işleminin "done" olarak tamamlanmış sayılması için hedef dizinin
mevcut olması VE kaynak dizinin silinmiş olması şartı getirildi. Ayrıca
"uploading" durumunun "queued" olarak güncellenmesi için hedefin
bulunmaması kontrolü eklendi.
2026-02-03 11:08:39 +03:00
7269f52b0e feat(rclone): rclone yükleme durumunu güncelle
Dosyaların GDrive'a taşınması sırasında yükleme durumunu izler
ve durum güncellemelerini yayınlar.
2026-02-03 10:50:58 +03:00
1a7a8ec66e refactor(ui): mount kontrol arayüzünü yeniden düzenle 2026-02-03 10:25:15 +03:00
1b0662a5ec refactor(rclone): akıllı temizleme özelliğini kaldır
Check-and-clean özelliğini kaldırıp sadece basit cache temizleme
özelliğini korudu. Kullanıcı arayüzünde tek bir "Cache Temizle"
butonu bulunuyor.
2026-02-03 09:26:54 +03:00
8825d0af8d feat(rclone): rclone ayar dosyası yolu desteği ekle 2026-02-03 09:02:14 +03:00
44323275d8 refactor(ui): kullanılmayan closeMenu fonksiyonunu kaldır 2026-02-02 22:54:23 +03:00
20da34beb2 feat(ui): silme işlemini iki aşamalı onay sistemine dönüştür
Tarayıcı doğrulama penceresi yerine inline onay mekanizması eklendi.
Kullanıcı dosya silmek için "Sil" butonuna ilk tıkladığında buton kırmızıya
dönerek "Emin misiniz?" sorusunu gösterir ve ikinci tıklamada silme işlemini
gerçekleştirir. Bu yaklaşım kullanıcı deneyimini iyileştirir ve uygulama
tutarlılığını artırır.
2026-02-02 22:49:26 +03:00
2b5bb86b3e feat(rclone): GDrive dosya silme desteği ekle
Silme API'si artık dosyaların konumunu otomatik olarak tespit edebiliyor
(DOWNLOAD_DIR veya GDRIVE_ROOT). GDrive dosyaları için doğrudan silme
mantığı uygulanırken, Downloads dosyaları için mevcut trash sistemi
korunuyor.
2026-02-02 22:30:38 +03:00
1e4fb38cfb feat(rclone): mount başlatma durumu ve log sunumunu geliştir
Mount işlemi için "Başlatılıyor" ara durumu eklenerek kullanıcı geri bildirimi
iyileştirildi. Sunucu tarafında log seviyeleri ayrıştırılarak gerçek hatalar
bilgi mesajlarından ayırt edildi ve arayüze yansıtıldı.
2026-02-02 22:14:41 +03:00
c61f1b0288 feat(rclone): akıllı cache yönetimi ve streaming performans ayarları ekle
Disk doluluk oranını izleyen ve otomatik temizleme yapan akıllı cache sistemi
eklendi. Streaming performansı için buffer size, VFS read ahead ve chunk size
ayarları yapılandırılabilir hale getirildi. Rclone crash durumunda otomatik
yeniden başlatma mekanizması eklendi. UI'da disk kullanım bilgileri ve VFS
cache modu görüntülenmeye başlandı.
2026-02-02 21:58:32 +03:00
e34b8fc024 fix(rclone): ilerleme takibini ve mount kontrolünü iyileştir
İlerleme güncellemelerinde artık hedef dosya/dizin GDrive'da mevcutsa
durum "done" olarak işaretleniyor. Transfer eşleştirmesi birden fazla
prefix desteği ile daha doğru çalışıyor. Cache temizleme işleminde
vfs/refresh kullanılıyor ve mount işlemlerinden önce aktiflik kontrolü
eklendi.
2026-02-02 19:04:04 +03:00
a95c844af9 docs(config): medya debug log açıklamasını güncelle 2026-02-02 16:34:10 +03:00
ca88b7816a fix(rclone): GDRIVE_ROOT yolunu düzelt 2026-02-02 16:31:33 +03:00
e44c21b36a refactor(rclone): cache temizlemeyi RC API üzerinden yap
Rclone cache temizleme işlemini artık mount durdurup başlatmak yerine
RC API'nin vfs/forget komutunu kullanarak yapar. Bu yöntem daha hızlı ve
daha güvenli bir temizleme sağlar. RC API devre dışıyken cache temizleme
işlemi yapılmaz ve uyarı döner. Ayrıca mount dizinindeki bağlantı
sorunları (ENOTCONN) için otomatik unmount mekanizması eklendi.
2026-02-02 15:51:18 +03:00
cd4769b3c1 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ı.
2026-02-02 15:26:16 +03:00
0fa3a818ae feat(rclone): Google Drive entegrasyonu ekle
Dockerfile ve docker-compose yapılandırması Rclone ve FUSE için güncellendi.
Backend API'leri Rclone durumunu, ayarlarını, yetkilendirmesini ve mount işlemlerini
yönetmek için eklendi. İndirmeler tamamlandığında (Torrent, YouTube, Mail.ru)
dosyaların otomatik veya manuel olarak Google Drive'a taşınması sağlandı.
Dosya sistemi hem yerel hem de mount edilmiş GDrive yollarını destekleyecek şekilde
güncellendi. Ayarlar ve Dosyalar arayüzüne ilgili kontroller eklendi.
2026-02-02 11:35:05 +03:00
e7aaea53ad fix(files): dosya taşıma ve yapıştırma işlemlerinde hedef yolunu düzelt
Hedef yolu null veya undefined olduğunda işlemin erken sonlanmasını engeller.
Hedef etiketi eksik olduğunda "Home" varsayılan değerini kullanır ve
normalizePath işlemini boş string ile devam ettirir.
2026-02-01 22:21:31 +03:00
11 changed files with 2287 additions and 191 deletions

View File

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

View File

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

View File

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

View 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>

View File

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

View File

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

View File

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

View 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);
}

View File

@@ -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
View File

@@ -0,0 +1,6 @@
{
"name": "dupe",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

File diff suppressed because it is too large Load Diff