Compare commits

29 Commits

Author SHA1 Message Date
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
6ac608a0b1 feat(movies): film tekrarlarını ve önbelleği temizle
Aynı film için birden fazla önbellek girdisi olduğunda en güncel olanı
tutup eski önbellekleri temizleyen mekanizma eklendi. Video yolu
bulunamayan filmlerin metadatası otomatik silinir.
2026-02-01 18:02:08 +03:00
e7925aa39f feat(webdav): film verilerini taşıma desteği ekle
Dosya taşıma işlemi sırasında etkilenen film verilerini ve metadatasını
korumak için yeni mantık eklendi. `collectMovieRelPathsForMove` ile
etkilenen yollar tespit edilirken, `moveMovieDataDir` ile fiziksel veri
klasörleri ve metadata.json dosyaları taşınarak `_dupe` referansları
güncelleniyor. Aynı kök dizin içinde veya farklı kök dizinler arasında
taşıma işlemleri destekleniyor.
2026-02-01 17:47:52 +03:00
e20b3ad591 feat(webdav): dizi metadatasını taşıma desteği ekle
Diziler ve bölümler kökler arası taşınırken ilişkili metadata
dosyalarının (.tvmetadata, series.json) güncellenmesini sağlar.
collectSeriesIdsForPath ile etkilenen dizileri tespit eder,
moveSeriesDataBetweenRoots ile metadata klasörlerini taşır ve
updateSeriesJsonAfterRootMove ile içindeki yolları günceller.
2026-02-01 17:35:53 +03:00
66aa99f0f7 feat(webdav): info.json tabanlı dizi indeksleme ekle
info.json dosyalarını okuyarak WebDAV dizin yapısını oluşturma
özelliği eklendi. Bu özellik, mevcut TV ve Anime veri köklerinde
olmayan ancak indirme dizininde bulunan dizileri indeksler.
2026-02-01 13:16:46 +03:00
b7014ee27e feat(webdav): webdav desteği ekle
webdav-server paketi kullanılarak WebDAV sunucusu entegre edildi.
Film, TV ve Anime dizinleri WebDAV istemcileri (örn. Infuse) için
otomatik olarak indekslenir ve sembolik bağlantılarla sunulur.
Yapılandırma, Basic Auth ve salt-okuma modu için yeni ortam
değişkenleri ve docker-compose ayarları eklendi.
2026-01-31 18:28:31 +03:00
3b98df4348 docs: tanım cümlesinden vurguyu kaldır 2026-01-31 10:39:00 +03:00
e937a67090 docs: arayüz açıklamasına vurgu ekle 2026-01-31 10:38:08 +03:00
569a7975de refactor(files): konum senkronizasyonunu sağlamlaştır
URL tabanlı konum yönetimini tek bir fonksiyon altında toplayarak
tarayıcı navigasyonu ve history API olaylarının tutarlı şekilde işlenmesini
sağla. pushState ve replaceState metodlarını patch ederek özel locationchange
olayı oluşturur ve bileşen yok edildiğinde patch işlemini geri alır.
2026-01-31 10:36:07 +03:00
5e6da2b445 chore(deps): client bağımlılıklarını güncelle 2026-01-31 10:35:28 +03:00
10 changed files with 4439 additions and 209 deletions

View File

@@ -31,3 +31,71 @@ AUTO_PAUSE_ON_COMPLETE=0
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır; # Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır. # CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
DISABLE_MEDIA_PROCESSING=0 DISABLE_MEDIA_PROCESSING=0
# WebDAV erişimi; Infuse gibi istemciler için salt-okuma paylaşımlar.
WEBDAV_ENABLED=1
# WebDAV Basic Auth kullanıcı adı.
WEBDAV_USERNAME=dupe
# WebDAV Basic Auth şifresi (güçlü bir parola kullanın).
WEBDAV_PASSWORD=superpassword
# WebDAV kök path'i (proxy üzerinden erişilecek).
WEBDAV_PATH=/webdav
# WebDAV salt-okuma modu.
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 # Build server
FROM node:22-slim 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 \ 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 && chmod a+rx /usr/local/bin/yt-dlp
WORKDIR /app/server WORKDIR /app/server

1292
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<script> <script>
import { onMount, tick } from "svelte"; import { onDestroy, onMount, tick } from "svelte";
import MatchModal from "../components/MatchModal.svelte"; import MatchModal from "../components/MatchModal.svelte";
import { API, apiFetch, moveEntry, renameFolder, copyEntry } from "../utils/api.js"; import { API, apiFetch, moveEntry, renameFolder, copyEntry } from "../utils/api.js";
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js"; import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
@@ -452,6 +452,7 @@
const VIEW_KEY = "filesViewMode"; const VIEW_KEY = "filesViewMode";
let viewMode = "grid"; let viewMode = "grid";
let initialPath = ""; let initialPath = "";
let unpatchHistory = null;
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const storedView = window.localStorage.getItem(VIEW_KEY); const storedView = window.localStorage.getItem(VIEW_KEY);
if (storedView === "grid" || storedView === "list") { if (storedView === "grid" || storedView === "list") {
@@ -467,6 +468,7 @@
let pendingPlayTarget = null; let pendingPlayTarget = null;
let activeMenu = null; // Aktif menü öğesi let activeMenu = null; // Aktif menü öğesi
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
let deleteConfirmPending = false; // Silme onayı beklemede mi
let showMatchModal = false; let showMatchModal = false;
let matchingFile = null; let matchingFile = null;
let matchTitle = ""; let matchTitle = "";
@@ -491,14 +493,16 @@
let clipboardItem = null; let clipboardItem = null;
let clipboardOperation = null; // 'cut' veya 'copy' let clipboardOperation = null; // 'cut' veya 'copy'
if (typeof window !== "undefined") { const syncFromLocation = ({ replace = false } = {}) => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const pathParam = params.get("path"); const pathParam = params.get("path");
let nextPath = "";
if (pathParam) { if (pathParam) {
try { try {
initialPath = normalizePath(decodeURIComponent(pathParam)); nextPath = normalizePath(decodeURIComponent(pathParam));
} catch (err) { } catch (err) {
initialPath = normalizePath(pathParam); nextPath = normalizePath(pathParam);
} }
} }
const playParam = params.get("play"); const playParam = params.get("play");
@@ -509,16 +513,49 @@
pendingPlayTarget = playParam; pendingPlayTarget = playParam;
} }
params.delete("play"); params.delete("play");
const search = params.toString();
const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
window.history.replaceState(
{ path: nextPath, originalPath: null },
"",
newUrl,
);
} }
const search = params.toString();
const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`; const state = window.history.state || {};
window.history.replaceState( const nextOriginal =
{ path: initialPath, originalPath: null }, typeof state.originalPath === "string"
"", ? normalizePath(state.originalPath)
newUrl, : resolveOriginalPathForDisplay(nextPath);
);
if (
normalizePath(currentPath) === nextPath &&
normalizePath(currentOriginalPath) === nextOriginal
) {
return;
}
const hadSearch = searchTerm.trim().length > 0;
const nextSearchTerm = hadSearch ? "" : searchTerm;
currentPath = nextPath;
currentOriginalPath = nextOriginal;
if (hadSearch) {
clearSearch("files");
}
selectedItems = new Set();
activeMenu = null;
if (isCreatingFolder) cancelCreateFolder();
updateVisibleState(files, nextPath);
renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm);
if (replace) {
updateUrlPath(nextPath, nextOriginal, { replace: true });
}
};
if (typeof window !== "undefined") {
syncFromLocation({ replace: true });
initialPath = currentPath;
} }
currentPath = normalizePath(initialPath);
// 🎬 Player kontrolleri // 🎬 Player kontrolleri
let videoEl; let videoEl;
let isPlaying = false; let isPlaying = false;
@@ -568,6 +605,27 @@
refreshMovieCount(); refreshMovieCount();
refreshTvShowCount(); 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) { function formatSize(bytes) {
if (!bytes) return "0 MB"; if (!bytes) return "0 MB";
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB"; if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
@@ -771,12 +829,12 @@
const sourcePath = resolveEntryOriginalPath(source); const sourcePath = resolveEntryOriginalPath(source);
const targetPath = resolveEntryOriginalPath(target); const targetPath = resolveEntryOriginalPath(target);
if (!sourcePath || !targetPath) return; if (!sourcePath || targetPath === undefined || targetPath === null) return;
const normalizedSource = normalizePath(sourcePath); const normalizedSource = normalizePath(sourcePath);
const normalizedTarget = normalizePath(targetPath); const normalizedTarget = normalizePath(targetPath || "");
if (!normalizedSource || !normalizedTarget) return; if (!normalizedSource) return;
if (source?.isDirectory) { if (source?.isDirectory) {
if ( if (
@@ -794,7 +852,8 @@
} }
const sourceLabel = cleanFileName(source.displayName || source.name || normalizedSource); 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?`)) { if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" içine taşımak istiyor musun?`)) {
return; return;
} }
@@ -1200,7 +1259,7 @@
const button = event.currentTarget; const button = event.currentTarget;
const rect = button.getBoundingClientRect(); const rect = button.getBoundingClientRect();
const menuWidth = 160; 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 // Üç noktanın son noktası ile menünün sol kenarını hizala
// Düğme genişliği 34px, son nokta sağ kenara yakın // Düğme genişliği 34px, son nokta sağ kenara yakın
@@ -1225,10 +1284,6 @@
}); });
} }
function closeMenu() {
activeMenu = null;
}
async function downloadFile(file) { async function downloadFile(file) {
if (!file || file.isDirectory) { if (!file || file.isDirectory) {
if (file?.isDirectory) navigateToPath(file.displayPath); if (file?.isDirectory) navigateToPath(file.displayPath);
@@ -1386,48 +1441,53 @@
async function deleteFile(item) { async function deleteFile(item) {
if (!item) return; 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(); closeMenu();
return; return;
} }
const label = // İlk tıklama - onay moduna geç
target.type === "directory" deleteConfirmPending = true;
? 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);
}
} }
await loadFiles(); // Menü kapandığında onay durumunu resetle
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]); function closeMenu() {
selectedItems = new Set( activeMenu = null;
[...selectedItems].filter((name) => name !== item.name), deleteConfirmPending = false;
); showMatchModal = false;
closeMenu(); matchingFile = null;
} }
// Klasör oluşturma fonksiyonları // Klasör oluşturma fonksiyonları
@@ -1608,15 +1668,15 @@
const sourcePath = resolveEntryOriginalPath(clipboardItem); const sourcePath = resolveEntryOriginalPath(clipboardItem);
const targetPath = resolveOriginalPathForDisplay(currentPath, currentOriginalPath); const targetPath = resolveOriginalPathForDisplay(currentPath, currentOriginalPath);
if (!sourcePath || !targetPath) { if (!sourcePath || targetPath === undefined || targetPath === null) {
alert("Geçersiz kaynak veya hedef yolu"); alert("Geçersiz kaynak veya hedef yolu");
return; return;
} }
const normalizedSource = normalizePath(sourcePath); const normalizedSource = normalizePath(sourcePath);
const normalizedTarget = normalizePath(targetPath); const normalizedTarget = normalizePath(targetPath || "");
if (!normalizedSource || !normalizedTarget) { if (!normalizedSource) {
alert("Geçersiz kaynak veya hedef yolu"); alert("Geçersiz kaynak veya hedef yolu");
return; return;
} }
@@ -1629,7 +1689,7 @@
} }
const sourceLabel = cleanFileName(clipboardItem.displayName || clipboardItem.name || normalizedSource); 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'; const actionLabel = clipboardOperation === 'cut' ? 'taşı' : 'kopyala';
if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" konumuna ${actionLabel}mak istiyor musun?`)) { if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" konumuna ${actionLabel}mak istiyor musun?`)) {
return; return;
@@ -1694,34 +1754,37 @@
}, 50); }, 50);
} }
}); });
const handlePopState = (event) => { const handlePopState = () => {
if (typeof window === "undefined") return; syncFromLocation();
const statePath =
event?.state && typeof event.state.path === "string"
? event.state.path
: null;
const stateOriginal =
event?.state && typeof event.state.originalPath === "string"
? event.state.originalPath
: null;
if (statePath !== null) {
currentPath = normalizePath(statePath);
} else {
const params = new URLSearchParams(window.location.search);
const paramPath = params.get("path");
currentPath = normalizePath(paramPath || "");
}
if (stateOriginal !== null) {
currentOriginalPath = normalizePath(stateOriginal);
} else {
currentOriginalPath = resolveOriginalPathForDisplay(
currentPath,
currentOriginalPath,
);
}
selectedItems = new Set();
activeMenu = null;
}; };
const handleLocationChange = () => {
syncFromLocation();
};
if (typeof window !== "undefined") {
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
const triggerChange = () => {
window.dispatchEvent(new Event("locationchange"));
};
window.history.pushState = function (...args) {
originalPushState.apply(this, args);
triggerChange();
};
window.history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
triggerChange();
};
window.addEventListener("locationchange", handleLocationChange);
window.addEventListener("popstate", handlePopState);
unpatchHistory = () => {
window.history.pushState = originalPushState;
window.history.replaceState = originalReplaceState;
window.removeEventListener("locationchange", handleLocationChange);
window.removeEventListener("popstate", handlePopState);
};
}
ws.onmessage = async (event) => { ws.onmessage = async (event) => {
try { try {
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data);
@@ -1949,6 +2012,7 @@
window.removeEventListener("click", handleClickOutside); window.removeEventListener("click", handleClickOutside);
window.removeEventListener("popstate", handlePopState); window.removeEventListener("popstate", handlePopState);
if (unpatchHistory) unpatchHistory();
// Resize observer'ı temizle // Resize observer'ı temizle
if (breadcrumbContainer) { if (breadcrumbContainer) {
@@ -2349,11 +2413,19 @@
<div class="menu-divider"></div> <div class="menu-divider"></div>
{/if} {/if}
<button <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)} on:click|stopPropagation={() => deleteFile(activeMenu)}
> >
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
<span>Sil</span> <span>{deleteConfirmPending ? 'Emin misiniz?' : 'Sil'}</span>
</button> </button>
</div> </div>
{/if} {/if}
@@ -3909,7 +3981,17 @@
.menu-item.delete { .menu-item.delete {
color: #e53935; 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 { .menu-item.delete:hover {
background-color: #ffebee; background-color: #ffebee;
} }

View File

@@ -5,6 +5,7 @@
const tabs = [ const tabs = [
{ id: "general", label: "General", icon: "fa-solid fa-sliders" }, { id: "general", label: "General", icon: "fa-solid fa-sliders" },
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" }, { 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" } { id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
]; ];
@@ -21,6 +22,15 @@
let error = null; let error = null;
let success = 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() { async function loadCookies() {
loadingCookies = true; loadingCookies = true;
error = null; error = null;
@@ -111,9 +121,143 @@
} }
} }
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;
success = 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}`);
}
success = "Rclone ayarları kaydedildi.";
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone ayarları kaydedilemedi.";
} finally {
rcloneSaving = false;
}
}
async function startRcloneMount() {
error = null;
success = 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
success = "Rclone mount başlatılıyor...";
// 2 saniye sonra status güncelle
setTimeout(async () => {
await loadRcloneStatus();
// Status yüklendikten sonra mesajı güncelle
if (rcloneStatus?.mounted) {
success = "Rclone mount başarıyla başlatıldı.";
} else if (rcloneStatus?.running) {
success = "Rclone mount başlatıldı, mount tamamlanıyor...";
} 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;
success = 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}`);
}
success = "Rclone mount durduruldu.";
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount durdurulamadı.";
}
}
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 = "Cache temizlendi.";
} catch (err) {
error = err?.message || "Cache temizlenemedi.";
}
}
onMount(() => { onMount(() => {
loadCookies(); loadCookies();
loadYoutubeSettings(); loadYoutubeSettings();
loadRcloneStatus();
loadRcloneConf();
}); });
function formatDate(ts) { function formatDate(ts) {
@@ -177,7 +321,7 @@
</label> </label>
</div> </div>
<div class="actions"> <div class="actions left">
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}> <button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-rotate"></i> Yenile <i class="fa-solid fa-rotate"></i> Yenile
</button> </button>
@@ -236,6 +380,155 @@
</div> </div>
{:else if activeTab === "general"} {:else if activeTab === "general"}
<div class="card muted">Genel ayarlar burada yer alacak.</div> <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}
{#if success}
<div class="alert success" style="margin-top:10px;">
<i class="fa-solid fa-circle-check"></i>
{success}
</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"} {:else if activeTab === "advanced"}
<div class="card muted">Gelişmiş ayarlar burada yer alacak.</div> <div class="card muted">Gelişmiş ayarlar burada yer alacak.</div>
{/if} {/if}
@@ -437,4 +730,40 @@
background: #e5ffe7; background: #e5ffe7;
color: #0f7a1f; 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> </style>

View File

@@ -9,6 +9,8 @@
let totalDownloaded = 0; let totalDownloaded = 0;
let totalDownloadSpeed = 0; let totalDownloadSpeed = 0;
let pollTimer; let pollTimer;
let moveToGdriveDefault = false;
let loadingRcloneStatus = false;
// Modal / player state // Modal / player state
let showModal = false; let showModal = false;
@@ -79,11 +81,28 @@
updateTransferStats(); 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) { async function upload(e) {
const f = e.target.files?.[0]; const f = e.target.files?.[0];
if (!f) return; if (!f) return;
const fd = new FormData(); const fd = new FormData();
fd.append("torrent", f); fd.append("torrent", f);
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅ await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅
await list(); await list();
} }
@@ -132,7 +151,7 @@
await apiFetch("/api/transfer", { await apiFetch("/api/transfer", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ magnet: input }) body: JSON.stringify({ magnet: input, moveToGdrive: moveToGdriveDefault })
}); });
await list(); await list();
return; return;
@@ -142,7 +161,7 @@
const resp = await apiFetch("/api/youtube/download", { const resp = await apiFetch("/api/youtube/download", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: normalizedYoutube }) body: JSON.stringify({ url: normalizedYoutube, moveToGdrive: moveToGdriveDefault })
}); });
if (!resp.ok) { if (!resp.ok) {
const data = await resp.json().catch(() => null); const data = await resp.json().catch(() => null);
@@ -157,7 +176,7 @@
const resp = await apiFetch("/api/mailru/download", { const resp = await apiFetch("/api/mailru/download", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: normalizedMailRu }) body: JSON.stringify({ url: normalizedMailRu, moveToGdrive: moveToGdriveDefault })
}); });
if (!resp.ok) { if (!resp.ok) {
const data = await resp.json().catch(() => null); 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) { function streamURL(hash, index = 0) {
const base = `${API}/stream/${hash}?index=${index}`; const base = `${API}/stream/${hash}?index=${index}`;
return withToken(base); return withToken(base);
@@ -581,6 +612,7 @@
for (const file of torrentsToUpload) { for (const file of torrentsToUpload) {
const fd = new FormData(); const fd = new FormData();
fd.append("torrent", file); fd.append("torrent", file);
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
await apiFetch("/api/transfer", { method: "POST", body: fd }); await apiFetch("/api/transfer", { method: "POST", body: fd });
} }
@@ -603,6 +635,7 @@
onMount(() => { onMount(() => {
list(); // 🔒 token'lı liste çekimi list(); // 🔒 token'lı liste çekimi
wsConnect(); // 🔒 token'lı WebSocket wsConnect(); // 🔒 token'lı WebSocket
loadRcloneStatus();
addGlobalDragListeners(); addGlobalDragListeners();
const slider = document.querySelector(".volume-slider"); const slider = document.querySelector(".volume-slider");
if (slider) { if (slider) {
@@ -642,6 +675,14 @@
<label class="btn-primary" on:click={handleUrlInput}> <label class="btn-primary" on:click={handleUrlInput}>
<i class="fa-solid fa-magnet btn-icon"></i> ADD URL <i class="fa-solid fa-magnet btn-icon"></i> ADD URL
</label> </label>
<label class="gdrive-toggle">
<input
type="checkbox"
bind:checked={moveToGdriveDefault}
disabled={loadingRcloneStatus}
/>
<span>GDrive'a taşı</span>
</label>
</div> </div>
<div style="display:flex; gap:10px;" title="Total Transfer Speed"> <div style="display:flex; gap:10px;" title="Total Transfer Speed">
<div class="transfer-info-box"> <div class="transfer-info-box">
@@ -734,6 +775,15 @@
{/if} {/if}
</div> </div>
<div style="display:flex; gap:5px;"> <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} {#if t.type === "torrent" || !t.type}
<button <button
class="toggle-btn" class="toggle-btn"
@@ -805,16 +855,24 @@
</div> </div>
{/if} {/if}
<div class="progress-bar"> <div class="progress-bar {t.moveStatus === 'uploading' ? 'uploading' : ''}">
<div <div
class="progress" class="progress"
style="width:{(t.progress || 0) * 100}%" style="width:{(t.moveStatus === 'uploading' ? (t.moveProgress || 0) : (t.progress || 0)) * 100}%"
></div> ></div>
</div> </div>
<div class="progress-text"> <div class="progress-text">
{#if t.type === "mailru" && t.status === "awaiting_match"} {#if t.type === "mailru" && t.status === "awaiting_match"}
Eşleştirme bekleniyor 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} {:else if (t.progress || 0) < 1}
{(t.progress * 100).toFixed(1)}% • {(t.progress * 100).toFixed(1)}% •
{t.downloaded ? (t.downloaded / 1e6).toFixed(1) : 0} MB • {t.downloaded ? (t.downloaded / 1e6).toFixed(1) : 0} MB •
@@ -1179,6 +1237,10 @@
transition: width 0.3s; transition: width 0.3s;
} }
.progress-bar.uploading .progress {
background: linear-gradient(90deg, #ef4444, #b91c1c);
}
.torrent-error { .torrent-error {
color: #e74c3c; color: #e74c3c;
font-size: 12px; font-size: 12px;
@@ -1502,4 +1564,26 @@
.more-item:hover { .more-item:hover {
background: #f1f5f9; 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> </style>

View File

@@ -7,7 +7,14 @@ services:
volumes: volumes:
- ./downloads:/app/server/downloads - ./downloads:/app/server/downloads
- ./cache:/app/server/cache - ./cache:/app/server/cache
- ./rclone:/config/rclone
restart: unless-stopped restart: unless-stopped
cap_add:
- SYS_ADMIN
security_opt:
- apparmor:unconfined
devices:
- /dev/fuse:/dev/fuse
# Login credentials for basic auth # Login credentials for basic auth
environment: environment:
USERNAME: ${USERNAME} USERNAME: ${USERNAME}
@@ -19,3 +26,34 @@ services:
DEBUG_CPU: ${DEBUG_CPU} DEBUG_CPU: ${DEBUG_CPU}
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE} AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING} DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}
WEBDAV_ENABLED: ${WEBDAV_ENABLED}
WEBDAV_USERNAME: ${WEBDAV_USERNAME}
WEBDAV_PASSWORD: ${WEBDAV_PASSWORD}
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": {}
}

View File

@@ -12,6 +12,7 @@
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"webdav-server": "^2.6.2",
"webtorrent": "^1.9.7", "webtorrent": "^1.9.7",
"ws": "^8.18.0" "ws": "^8.18.0"
} }

File diff suppressed because it is too large Load Diff