Compare commits
29 Commits
ph
...
a011af7368
| Author | SHA1 | Date | |
|---|---|---|---|
| a011af7368 | |||
| 90587aa6d6 | |||
| c3d38d2e79 | |||
| 2b9c776c8a | |||
| 7269f52b0e | |||
| 1a7a8ec66e | |||
| 1b0662a5ec | |||
| 8825d0af8d | |||
| 44323275d8 | |||
| 20da34beb2 | |||
| 2b5bb86b3e | |||
| 1e4fb38cfb | |||
| c61f1b0288 | |||
| e34b8fc024 | |||
| a95c844af9 | |||
| ca88b7816a | |||
| e44c21b36a | |||
| cd4769b3c1 | |||
| 0fa3a818ae | |||
| e7aaea53ad | |||
| 6ac608a0b1 | |||
| e7925aa39f | |||
| e20b3ad591 | |||
| 66aa99f0f7 | |||
| b7014ee27e | |||
| 3b98df4348 | |||
| e937a67090 | |||
| 569a7975de | |||
| 5e6da2b445 |
68
.env.example
68
.env.example
@@ -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
|
||||||
|
|||||||
@@ -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
1292
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "dupe",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
2552
server/server.js
2552
server/server.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user