Compare commits

25 Commits

Author SHA1 Message Date
0255a44120 fix(client-server): rclone ve miniplayer düzeltmeleri 2026-02-04 00:36:46 +03:00
42447a63f3 refactor(ui): mini player kapak (video play alanı) düzenlemeleri 2026-02-03 18:45:56 +03:00
f9c0cc15f0 refactor(ui): mini player kapak (video play alanı) düzenlemeleri 2026-02-03 18:40:16 +03:00
90f6291e14 refactor(ui): mini player düzenlemesi
Kullanıcı meta verilerini kapağın altına taşıyarak mini player'ın düzenini yeniden düzenledi. Paylaş ve beğen butonlarını kaldırdı.
2026-02-03 17:20:58 +03:00
7753b68578 feat(ui): music sayfasında mini player'ı gizle _3 2026-02-03 16:56:11 +03:00
6fc2d2c45f feat(ui): music sayfasında mini player'ı gizle _2 2026-02-03 16:53:15 +03:00
e66ace9ed5 feat(ui): music sayfasında mini player'ı gizle 2026-02-03 16:49:01 +03:00
1774f681be fix(server): youtube müzik videolarının stream etme sorununu çöz
Küçük resim ve video akışı için dosya yolları çözülürken
`resolveStoragePath` fonksiyonu kullanılmaya başlandı. Bu sayede
dosyalar standart indirme dizininde veya rclone ile yönetilen
bir konumda olsa bile doğru şekilde erişilebilir.
2026-02-03 16:39:49 +03:00
2c06867c75 style(ui): toats güncelleme 2026-02-03 13:15:30 +03:00
2d06056c1a style(ui): sekmeler için sınıf atamasını güncelle 2026-02-03 13:10:20 +03:00
92cdb4ee61 refactor(ui): toast bileşeninde reactive store kullan 2026-02-03 12:39:47 +03:00
d705e37d85 feat(ui): toast bildirim sistemini ekle
Toast bileşeni ve store oluşturuldu. Ayarlar sayfasındaki işlem bildirimleri yeni sistemle gösterilmeye başlandı.
2026-02-03 12:34:49 +03:00
2f3dc72dcc chore(rclone): mailru transfer eşleşme debug logu ekle
MailRu yüklemelerinde transfer eşleşmesi bulunamadığında sorunun
teşhisi için job ve transfer detaylarını içeren uyarı logu eklendi.
2026-02-03 12:03:48 +03:00
a011af7368 fix(rclone): mailru için fileName fallback ekle 2026-02-03 11:53:47 +03:00
90587aa6d6 fix(rclone): avatar dosya yolunu düzelt 2026-02-03 11:33:27 +03:00
c3d38d2e79 fix(rclone): aktarım durum takibini düzelt
Dosya sistemi tabanlı tamamlanma kontrolünü kaldırır.
Aktarım listesinde olmayan "uploading" durumundaki öğeleri "queued"ye
çevirir. Bu sayede polling süreci devam eder ve tamamlanma kararı
aktarım listesinin boşalmasına bırakılır.
2026-02-03 11:23:30 +03:00
2b9c776c8a fix(rclone): taşıma tamamlanma koşullarını düzelt
Taşıma işleminin "done" olarak tamamlanmış sayılması için hedef dizinin
mevcut olması VE kaynak dizinin silinmiş olması şartı getirildi. Ayrıca
"uploading" durumunun "queued" olarak güncellenmesi için hedefin
bulunmaması kontrolü eklendi.
2026-02-03 11:08:39 +03:00
7269f52b0e feat(rclone): rclone yükleme durumunu güncelle
Dosyaların GDrive'a taşınması sırasında yükleme durumunu izler
ve durum güncellemelerini yayınlar.
2026-02-03 10:50:58 +03:00
1a7a8ec66e refactor(ui): mount kontrol arayüzünü yeniden düzenle 2026-02-03 10:25:15 +03:00
1b0662a5ec refactor(rclone): akıllı temizleme özelliğini kaldır
Check-and-clean özelliğini kaldırıp sadece basit cache temizleme
özelliğini korudu. Kullanıcı arayüzünde tek bir "Cache Temizle"
butonu bulunuyor.
2026-02-03 09:26:54 +03:00
8825d0af8d feat(rclone): rclone ayar dosyası yolu desteği ekle 2026-02-03 09:02:14 +03:00
44323275d8 refactor(ui): kullanılmayan closeMenu fonksiyonunu kaldır 2026-02-02 22:54:23 +03:00
20da34beb2 feat(ui): silme işlemini iki aşamalı onay sistemine dönüştür
Tarayıcı doğrulama penceresi yerine inline onay mekanizması eklendi.
Kullanıcı dosya silmek için "Sil" butonuna ilk tıkladığında buton kırmızıya
dönerek "Emin misiniz?" sorusunu gösterir ve ikinci tıklamada silme işlemini
gerçekleştirir. Bu yaklaşım kullanıcı deneyimini iyileştirir ve uygulama
tutarlılığını artırır.
2026-02-02 22:49:26 +03:00
2b5bb86b3e feat(rclone): GDrive dosya silme desteği ekle
Silme API'si artık dosyaların konumunu otomatik olarak tespit edebiliyor
(DOWNLOAD_DIR veya GDRIVE_ROOT). GDrive dosyaları için doğrudan silme
mantığı uygulanırken, Downloads dosyaları için mevcut trash sistemi
korunuyor.
2026-02-02 22:30:38 +03:00
1e4fb38cfb feat(rclone): mount başlatma durumu ve log sunumunu geliştir
Mount işlemi için "Başlatılıyor" ara durumu eklenerek kullanıcı geri bildirimi
iyileştirildi. Sunucu tarafında log seviyeleri ayrıştırılarak gerçek hatalar
bilgi mesajlarından ayırt edildi ve arayüze yansıtıldı.
2026-02-02 22:14:41 +03:00
10 changed files with 769 additions and 254 deletions

View File

@@ -97,3 +97,5 @@ RCLONE_AUTO_RESTART=1
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

@@ -1,9 +1,11 @@
<script>
import { Router, Route } from "svelte-routing";
import { useLocation } from "svelte-routing";
import { onMount } from "svelte";
import Sidebar from "./components/Sidebar.svelte";
import Topbar from "./components/Topbar.svelte";
import MiniPlayer from "./components/MiniPlayer.svelte";
import Toast from "./components/Toast.svelte";
import Files from "./routes/Files.svelte";
import Transfers from "./routes/Transfers.svelte";
import Trash from "./routes/Trash.svelte";
@@ -27,6 +29,8 @@
let menuOpen = false;
let wsCounts;
let refreshTimer = null;
const location = useLocation();
$: isMusicRoute = ($location?.pathname || "").startsWith("/music");
const scheduleMediaRefresh = () => {
if (refreshTimer) return;
@@ -162,7 +166,10 @@
<Route path="/trash" component={Trash} />
</div>
<MiniPlayer />
{#if !isMusicRoute}
<MiniPlayer />
{/if}
<Toast />
<!-- Sidebar dışına tıklayınca kapanma -->
{#if menuOpen}

View File

@@ -1,9 +1,25 @@
<script>
import { onMount, afterUpdate, tick } from "svelte";
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
import { useLocation } from "svelte-routing";
import { API, withToken } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
let videoEl = null;
let titleWrap = null;
let titleInner = null;
let marqueeShift = "0px";
let marqueeDuration = "0s";
let marqueeEnabled = false;
let dragX = 0;
let dragY = 0;
let dragging = false;
let dragStartX = 0;
let dragStartY = 0;
let originX = 0;
let originY = 0;
const location = useLocation();
$: isMusicRoute = ($location?.pathname || "").startsWith("/music");
function thumbnailURL(item) {
if (!item?.thumbnail) return null;
@@ -36,39 +52,97 @@
return withToken(`${API}/stream/${item.infoHash}?index=${index}`);
}
const HARD_SYNC_THRESHOLD = 0.2;
const SOFT_SYNC_THRESHOLD = 0.05;
$: if (videoEl && $musicPlayer.currentTrack && $musicPlayer.isPlaying) {
const target = $musicPlayer.currentTime || 0;
if (Number.isFinite(target) && Math.abs(videoEl.currentTime - target) > 0.6) {
videoEl.currentTime = target;
if (!Number.isFinite(target)) {
videoEl.playbackRate = 1;
} else {
const delta = target - (videoEl.currentTime || 0);
if (Math.abs(delta) > HARD_SYNC_THRESHOLD) {
videoEl.currentTime = target;
} else if (Math.abs(delta) > SOFT_SYNC_THRESHOLD) {
videoEl.playbackRate = delta > 0 ? 1.02 : 0.98;
} else {
videoEl.playbackRate = 1;
}
}
}
async function updateMarquee() {
if (!titleWrap || !titleInner) return;
await tick();
const wrapW = titleWrap.clientWidth || 0;
const textW = titleInner.scrollWidth || 0;
if (textW > wrapW + 2) {
const shift = Math.max(textW - wrapW, 0);
marqueeShift = `${shift}px`;
marqueeDuration = `${Math.max(8, shift / 25)}s`;
marqueeEnabled = true;
} else {
marqueeShift = "0px";
marqueeDuration = "0s";
marqueeEnabled = false;
}
}
onMount(() => {
updateMarquee();
});
afterUpdate(() => {
updateMarquee();
});
$: if ($musicPlayer.currentTrack?.id) {
setTimeout(updateMarquee, 0);
}
$: if (videoEl && $musicPlayer.isPlaying) {
videoEl.play().catch(() => undefined);
} else if (videoEl) {
videoEl.pause();
}
function shouldIgnoreDrag(target) {
if (!target) return false;
return Boolean(target.closest("button, a, input, textarea, select, [data-no-drag]"));
}
function startDrag(event) {
if (shouldIgnoreDrag(event.target)) return;
dragging = true;
dragStartX = event.clientX;
dragStartY = event.clientY;
originX = dragX;
originY = dragY;
window.addEventListener("pointermove", onDrag);
window.addEventListener("pointerup", endDrag, { once: true });
}
function onDrag(event) {
if (!dragging) return;
dragX = originX + (event.clientX - dragStartX);
dragY = originY + (event.clientY - dragStartY);
}
function endDrag() {
dragging = false;
window.removeEventListener("pointermove", onDrag);
}
</script>
{#if $musicPlayer.currentTrack}
<div class="mini-player" aria-label="Mini music player">
<div class="mini-top">
<div class="mini-user-meta">
<div class="mini-user-name">
{cleanFileName($musicPlayer.currentTrack.title)}
</div>
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
</div>
<div class="mini-actions">
<button class="mini-icon-btn" title="Paylaş">
<i class="fa-solid fa-arrow-up-from-bracket"></i>
</button>
<button class="mini-icon-btn" title="Beğen">
<i class="fa-regular fa-heart"></i>
</button>
</div>
</div>
{#if $musicPlayer.currentTrack && !isMusicRoute}
<div
class="mini-player {dragging ? 'dragging' : ''}"
aria-label="Mini music player"
on:pointerdown={startDrag}
style="transform: translate({dragX}px, {dragY}px);"
>
<div class="mini-top"></div>
<div class="mini-cover">
{#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)}
@@ -90,6 +164,20 @@
</div>
{/if}
</div>
<div class="mini-meta">
<div class="mini-user-name" bind:this={titleWrap}>
{#key $musicPlayer.currentTrack?.id}
<span
class="marquee {marqueeEnabled ? 'active' : ''}"
bind:this={titleInner}
style="--marquee-shift: {marqueeShift}; --marquee-duration: {marqueeDuration};"
>
{cleanFileName($musicPlayer.currentTrack.title)}
</span>
{/key}
</div>
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
</div>
<div class="mini-progress">
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
@@ -105,20 +193,20 @@
</div>
<div class="mini-controls">
<button class="mini-btn" on:click={playPrevious} title="Önceki">
<button class="mini-btn" on:click={playPrevious} title="Önceki" data-no-drag>
<i class="fa-solid fa-backward-step"></i>
</button>
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur">
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur" data-no-drag>
{#if $musicPlayer.isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<button class="mini-btn" on:click={playNext} title="Sonraki">
<button class="mini-btn" on:click={playNext} title="Sonraki" data-no-drag>
<i class="fa-solid fa-forward-step"></i>
</button>
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat">
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat" data-no-drag>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@@ -139,18 +227,21 @@
color: #f6f6f6;
backdrop-filter: blur(14px);
z-index: 50;
cursor: grab;
touch-action: none;
}
.mini-player.dragging {
cursor: grabbing;
}
.mini-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 0;
}
.mini-user-meta {
min-width: 0;
.mini-meta {
text-align: left;
margin: 0 4px 8px -4px;
}
.mini-user-name {
@@ -159,6 +250,19 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
position: relative;
max-width: 100%;
min-width: 0;
}
.mini-user-name .marquee {
display: inline-block;
will-change: transform;
}
.mini-user-name .marquee.active {
animation: mini-marquee var(--marquee-duration) linear infinite;
}
.mini-user-handle {
@@ -166,39 +270,55 @@
color: rgba(255, 255, 255, 0.6);
}
.mini-actions {
display: flex;
gap: 8px;
}
.mini-icon-btn {
width: 32px;
height: 32px;
border-radius: 12px;
border: none;
background: rgba(255, 255, 255, 0.12);
color: #f6f6f6;
cursor: pointer;
@keyframes mini-marquee {
0% {
transform: translateX(0);
opacity: 1;
}
10% {
transform: translateX(0);
opacity: 1;
}
60% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 1;
}
80% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 1;
}
84% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 0;
}
86% {
transform: translateX(0);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.mini-cover {
margin: 14px auto 12px;
width: 160px;
height: 160px;
border-radius: 20px;
margin: -16px -16px 12px;
width: calc(100% + 32px);
aspect-ratio: 1 / 1;
border-radius: 26px 26px 0 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
display: grid;
place-items: center;
transform: translateY(-6px);
border: none;
display: block;
}
.mini-cover video,
.mini-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.mini-cover video {

View File

@@ -0,0 +1,76 @@
<script>
import { toast } from '../stores/toastStore.js';
import { fade } from 'svelte/transition';
const icons = {
success: 'fa-solid fa-circle-check',
error: 'fa-solid fa-circle-exclamation',
info: 'fa-solid fa-circle-info'
};
</script>
{#if $toast.visible && $toast.message}
<div class="toast-container" transition:fade={{ duration: 200 }}>
<div class="toast {$toast.type}">
<i class="{icons[$toast.type] || icons.info}"></i>
<span>{$toast.message}</span>
</div>
</div>
{/if}
<style>
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9999;
max-width: min(350px, 90vw);
}
.toast {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.toast i {
font-size: 18px;
flex-shrink: 0;
}
.toast.success {
background: linear-gradient(135deg, rgba(0, 200, 83, 0.95), rgba(0, 150, 63, 0.95));
color: white;
}
.toast.error {
background: linear-gradient(135deg, rgba(220, 38, 38, 0.95), rgba(185, 28, 28, 0.95));
color: white;
}
.toast.info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(37, 99, 235, 0.95));
color: white;
}
@media (max-width: 640px) {
.toast-container {
bottom: 16px;
right: 16px;
left: 16px;
max-width: none;
}
.toast {
padding: 12px 16px;
font-size: 13px;
}
}
</style>

View File

@@ -468,6 +468,7 @@
let pendingPlayTarget = null;
let activeMenu = null; // Aktif menü öğesi
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
let deleteConfirmPending = false; // Silme onayı beklemede mi
let showMatchModal = false;
let matchingFile = null;
let matchTitle = "";
@@ -625,6 +626,25 @@
alert(err?.message || "GDrive taşıma başarısız oldu.");
}
}
async function setCategory(entry, category) {
if (!entry?.name) return;
try {
const resp = await apiFetch("/api/file/category", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: entry.name, category })
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
alert(data?.error || "Kategori güncellenemedi.");
return;
}
await loadFiles();
} catch (err) {
alert(err?.message || "Kategori güncellenemedi.");
}
}
function formatSize(bytes) {
if (!bytes) return "0 MB";
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
@@ -1283,10 +1303,6 @@
});
}
function closeMenu() {
activeMenu = null;
}
async function downloadFile(file) {
if (!file || file.isDirectory) {
if (file?.isDirectory) navigateToPath(file.displayPath);
@@ -1304,45 +1320,55 @@
}
function matchFile(file) {
if (!file || file.isDirectory) {
closeMenu();
return;
}
// Dosya adını al (path'in son kısmı)
const fileName = file.name.split('/').pop();
// Önce dizi kontrolü yap (SxxExx formatı)
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (seriesMatch) {
matchType = "series";
const { title, year } = extractTitleAndYear(fileName);
matchTitle = title || fileName;
matchYear = year ? String(year) : "";
} else {
// Film kontrolü (yıl bilgisi)
const { title, year } = extractTitleAndYear(fileName);
if (year && year >= 1900 && year <= 2099) {
matchType = "movie";
matchTitle = title || fileName;
matchYear = String(year);
} else {
// Varsayılan olarak film kabul et
matchType = "movie";
matchTitle = title || fileName;
matchYear = "";
try {
if (!file || file.isDirectory) {
closeMenu();
return;
}
const rawName = file?.name || file?.displayName || "";
const fileName = rawName.split("/").pop();
if (!fileName) {
showToast("Eşleştirilecek dosya adı bulunamadı.", "error");
closeMenu();
return;
}
// Önce dizi kontrolü yap (SxxExx formatı)
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (seriesMatch) {
matchType = "series";
const { title, year } = extractTitleAndYear(fileName);
matchTitle = title || fileName;
matchYear = year ? String(year) : "";
} else {
// Film kontrolü (yıl bilgisi)
const { title, year } = extractTitleAndYear(fileName);
if (year && year >= 1900 && year <= 2099) {
matchType = "movie";
matchTitle = title || fileName;
matchYear = String(year);
} else {
// Varsayılan olarak film kabul et
matchType = "movie";
matchTitle = title || fileName;
matchYear = "";
}
}
matchingFile = file;
showMatchModal = true;
closeMenu();
// Modal açıldıktan sonra otomatik arama yap
tick().then(() => {
searchMetadata();
});
} catch (err) {
showToast("Eşleştirme penceresi açılamadı.", "error");
closeMenu();
}
matchingFile = file;
showMatchModal = true;
closeMenu();
// Modal açıldıktan sonra otomatik arama yap
tick().then(() => {
searchMetadata();
});
}
function closeMatchModal() {
@@ -1373,6 +1399,10 @@
params.set("year", matchYear);
}
const token = localStorage.getItem("token");
if (token) {
params.set("token", token);
}
const response = await apiFetch(`/api/search/metadata?${params}`);
if (response.ok) {
@@ -1444,48 +1474,51 @@
async function deleteFile(item) {
if (!item) return;
const target = resolveDeletionTargets(item);
if (!target) {
// Eğer onay beklemedeyse, silme işlemini gerçekleştir
if (deleteConfirmPending) {
const target = resolveDeletionTargets(item);
if (!target) {
closeMenu();
return;
}
const result = await performDeletion(target);
deleteConfirmPending = false; // Reset flag
if (!result.ok) {
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
closeMenu();
return;
}
if (item.isDirectory) {
const displayKey = normalizePath(
item.displayPath ||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
);
if (displayKey || displayKey === "") {
pendingFolders.delete(displayKey);
}
}
await loadFiles();
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
selectedItems = new Set(
[...selectedItems].filter((name) => name !== item.name),
);
closeMenu();
return;
}
const label =
target.type === "directory"
? target.label || item.displayName || "Klasör"
: target.label || cleanFileName(item.name);
const message =
target.type === "directory"
? `"${label}" klasörünü silmek istediğine emin misin?`
: `"${label}" dosyasını silmek istediğinizden emin misiniz?`;
if (!confirm(message)) {
closeMenu();
return;
}
const result = await performDeletion(target);
if (!result.ok) {
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
closeMenu();
return;
}
if (item.isDirectory) {
const displayKey = normalizePath(
item.displayPath ||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
);
if (displayKey || displayKey === "") {
pendingFolders.delete(displayKey);
}
// İlk tıklama - onay moduna geç
deleteConfirmPending = true;
}
await loadFiles();
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
selectedItems = new Set(
[...selectedItems].filter((name) => name !== item.name),
);
closeMenu();
// Menü kapandığında onay durumunu resetle
function closeMenu() {
activeMenu = null;
deleteConfirmPending = false;
}
// Klasör oluşturma fonksiyonları
@@ -2393,6 +2426,20 @@
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Eşleştir</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => setCategory(activeMenu, "music")}
>
<i class="fa-solid fa-music"></i>
<span>Kategori: Müzik</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => setCategory(activeMenu, "auto")}
>
<i class="fa-solid fa-rotate-left"></i>
<span>Kategori: Otomatik</span>
</button>
<div class="menu-divider"></div>
<button
class="menu-item"
@@ -2419,11 +2466,11 @@
</button>
<div class="menu-divider"></div>
<button
class="menu-item delete"
class="menu-item delete {deleteConfirmPending ? 'confirming' : ''}"
on:click|stopPropagation={() => deleteFile(activeMenu)}
>
<i class="fa-solid fa-trash"></i>
<span>Sil</span>
<span>{deleteConfirmPending ? 'Emin misiniz?' : 'Sil'}</span>
</button>
</div>
{/if}
@@ -3245,12 +3292,14 @@
.name {
font-weight: 600;
font-size: 13px;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
max-height: calc(1.3em * 2);
min-height: calc(1.3em * 2);
text-overflow: ellipsis;
word-break: break-word;
}
@@ -3821,7 +3870,8 @@
.folder-info {
margin-top: 4px;
width: 100%;
text-align: center;
text-align: left;
padding: 0 8px;
flex-shrink: 0;
}
.folder-name {
@@ -3833,8 +3883,9 @@
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: center;
text-align: left;
max-height: calc(1.25em * 2);
min-height: calc(1.25em * 2);
overflow: hidden;
text-overflow: ellipsis;
}
@@ -3979,7 +4030,17 @@
.menu-item.delete {
color: #e53935;
}
.menu-item.delete.confirming {
color: #fff;
background-color: #e53935;
font-weight: 600;
}
.menu-item.delete.confirming:hover {
background-color: #c62828;
}
.menu-item.delete:hover {
background-color: #ffebee;
}

View File

@@ -1,12 +1,13 @@
<script>
import { onMount } from "svelte";
import { apiFetch } from "../utils/api.js";
import { showToast } from "../stores/toastStore.js";
const tabs = [
{ id: "general", label: "General", icon: "fa-solid fa-sliders" },
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" },
{ id: "rclone", label: "Rclone", icon: "fa-solid fa-cloud" },
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
{ id: "general", label: "General", icon: "sliders" },
{ id: "youtube", label: "YouTube", icon: "youtube" },
{ id: "rclone", label: "Rclone", icon: "cloud" },
{ id: "advanced", label: "Advanced", icon: "gear" }
];
let activeTab = "youtube";
@@ -20,7 +21,6 @@
let onlyAudio = false;
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
let error = null;
let success = null;
let rcloneStatus = null;
let rcloneLoading = false;
@@ -34,7 +34,6 @@
async function loadCookies() {
loadingCookies = true;
error = null;
success = null;
try {
const resp = await apiFetch("/api/youtube/cookies");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
@@ -51,7 +50,6 @@
async function saveCookies() {
if (savingCookies) return;
error = null;
success = null;
savingCookies = true;
try {
const payload = {
@@ -71,7 +69,7 @@
throw new Error(data?.error || `HTTP ${resp.status}`);
}
cookiesUpdatedAt = data.updatedAt || Date.now();
success = "Cookies kaydedildi.";
showToast("Cookies kaydedildi.", "success");
} catch (err) {
error = err?.message || "Cookies kaydedilemedi.";
} finally {
@@ -99,7 +97,6 @@
if (savingYtSettings) return;
savingYtSettings = true;
error = null;
success = null;
try {
const resp = await apiFetch("/api/youtube/settings", {
method: "POST",
@@ -113,7 +110,7 @@
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "YouTube indirme ayarları kaydedildi.";
showToast("YouTube indirme ayarları kaydedildi.", "success");
} catch (err) {
error = err?.message || "YouTube ayarları kaydedilemedi.";
} finally {
@@ -156,7 +153,6 @@
if (rcloneSaving) return;
rcloneSaving = true;
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/settings", {
method: "POST",
@@ -180,7 +176,7 @@
if (!confResp.ok || !confData?.ok) {
throw new Error(confData?.error || `HTTP ${confResp.status}`);
}
success = "Rclone ayarları kaydedildi.";
showToast("Rclone ayarları kaydedildi.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone ayarları kaydedilemedi.";
@@ -191,14 +187,30 @@
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}`);
}
success = "Rclone mount başlatıldı.";
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
showToast("Rclone mount başlatılıyor...", "info");
// 2 saniye sonra status güncelle
setTimeout(async () => {
await loadRcloneStatus();
// Status yüklendikten sonra mesajı güncelle
if (rcloneStatus?.mounted) {
showToast("Rclone mount başarıyla başlatıldı.", "success");
} else if (rcloneStatus?.running) {
showToast("Rclone mount başlatıldı, mount tamamlanıyor...", "info");
} else {
error = "Rclone mount başlatılamadı.";
}
}, 2000);
// İlk status güncellemesi
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount başlatılamadı.";
@@ -207,14 +219,13 @@
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.";
showToast("Rclone mount durduruldu.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount durdurulamadı.";
@@ -223,35 +234,17 @@
async function cleanRcloneCache() {
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "Rclone cache temizlendi.";
showToast("Cache temizlendi.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone cache temizlenemedi.";
}
}
async function checkAndCleanCache() {
error = null;
success = null;
try {
const resp = await apiFetch("/api/rclone/cache/check-and-clean", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(data?.error || data?.message || `HTTP ${resp.status}`);
}
if (data.cleaned) {
success = data.message || "Cache temizlendi.";
} else {
success = data.message || "Disk durumu iyi, temizleme gerekmedi.";
}
} catch (err) {
error = err?.message || "Cache kontrolü başarısız.";
error = err?.message || "Cache temizlenemedi.";
showToast(error, "error");
}
}
@@ -278,17 +271,38 @@
</div>
<div class="tabs">
{#each tabs as tab}
<button
class:active={activeTab === tab.id}
class="tab"
type="button"
on:click={() => (activeTab = tab.id)}
>
<i class={tab.icon}></i>
<span>{tab.label}</span>
</button>
{/each}
<button
class="tab {activeTab === 'general' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'general'}
>
<i class="fa-solid fa-sliders"></i>
<span>General</span>
</button>
<button
class="tab {activeTab === 'youtube' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'youtube'}
>
<i class="fa-brands fa-youtube"></i>
<span>YouTube</span>
</button>
<button
class="tab {activeTab === 'rclone' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'rclone'}
>
<i class="fa-solid fa-cloud"></i>
<span>Rclone</span>
</button>
<button
class="tab {activeTab === 'advanced' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'advanced'}
>
<i class="fa-solid fa-gear"></i>
<span>Advanced</span>
</button>
</div>
{#if activeTab === "youtube"}
@@ -373,12 +387,6 @@
{error}
</div>
{/if}
{#if success}
<div class="alert success">
<i class="fa-solid fa-circle-check"></i>
{success}
</div>
{/if}
</div>
{:else if activeTab === "general"}
<div class="card muted">Genel ayarlar burada yer alacak.</div>
@@ -422,11 +430,8 @@
disabled={rcloneLoading || rcloneSaving}
/>
</div>
<button class="btn" on:click={cleanRcloneCache}>
<i class="fa-solid fa-broom"></i> Temizle
</button>
<button class="btn primary" on:click={checkAndCleanCache}>
<i class="fa-solid fa-wand-magic-sparkles"></i> Akıllı Temizle
<button class="btn primary" on:click={cleanRcloneCache}>
<i class="fa-solid fa-broom"></i> Cache Temizle
</button>
</div>
@@ -455,7 +460,24 @@
</button>
</div>
<div class="actions">
{#if error}
<div class="alert error" style="margin-top:10px;">
<i class="fa-solid fa-circle-exclamation"></i>
{error}
</div>
{/if}
</div>
<!-- Mount Kontrol Kartı -->
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-solid fa-cloud"></i>
<span>Mount Kontrol</span>
</div>
</div>
<div class="actions left">
<button class="btn" on:click={startRcloneMount}>
<i class="fa-solid fa-play"></i> Mount Başlat
</button>
@@ -465,10 +487,20 @@
</div>
{#if rcloneStatus}
<div class="card muted" style="margin-top:10px;">
<div class="card muted" style="margin-top:12px;">
<div><strong>Durum:</strong></div>
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
<div>Mounted: {rcloneStatus.mounted ? "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>
@@ -489,21 +521,15 @@
</div>
{/if}
{#if rcloneStatus.lastError}
<div style="margin-top: 8px; color: #d32f2f;">Son hata: {rcloneStatus.lastError}</div>
<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}
{#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>

View File

@@ -0,0 +1,24 @@
import { writable } from 'svelte/store';
export const toast = writable({
message: null,
type: 'success', // success, error, info
visible: false
});
let toastTimeout = null;
export function showToast(message, type = 'success', duration = 3000) {
// Önceki toast'ı temizle
if (toastTimeout) {
clearTimeout(toastTimeout);
}
// Yeni toast'ı göster
toast.set({ message, type, visible: true });
// Belirli süre sonra gizle
toastTimeout = setTimeout(() => {
toast.set({ message: null, type: 'success', visible: false });
}, duration);
}

View File

@@ -56,3 +56,4 @@ services:
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

@@ -186,7 +186,7 @@ const FFPROBE_MAX_BUFFER =
Number(process.env.FFPROBE_MAX_BUFFER) > 0
? Number(process.env.FFPROBE_MAX_BUFFER)
: 10 * 1024 * 1024;
const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png");
const AVATAR_PATH = path.join(__dirname, "cache", "avatar.png");
function getWsClientCount() {
if (!wss) return 0;
@@ -723,6 +723,16 @@ function readInfoForRoot(rootFolder) {
return null;
}
function getInfoPathForRoot(rootFolder) {
const safe = sanitizeRelative(rootFolder);
if (!safe) return null;
const localPath = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
if (fs.existsSync(localPath)) return localPath;
const gdrivePath = path.join(GDRIVE_ROOT, safe, INFO_FILENAME);
if (fs.existsSync(gdrivePath)) return gdrivePath;
return localPath;
}
function sanitizeRelative(relPath) {
return relPath.replace(/^[\\/]+/, "");
}
@@ -780,6 +790,7 @@ function resolveRootDir(rootFolder) {
let rcloneProcess = null;
let rcloneLastError = null;
let rcloneLastLogMessage = null; // Tüm log mesajları için (NOTICE dahil)
const rcloneAuthSessions = new Map();
let rcloneCacheCleanTimer = null;
// Auto-restart sayaçları
@@ -887,14 +898,10 @@ function updateMoveProgressFromStats(stats) {
updated = true;
}
} else {
const gdriveTarget = relRoot ? path.join(GDRIVE_ROOT, relRoot) : null;
const targetExists = gdriveTarget ? fs.existsSync(gdriveTarget) : false;
if (targetExists) {
entry.moveStatus = "done";
entry.moveProgress = 1;
updated = true;
} else if (entry.moveStatus === "uploading") {
// Transfer görünmüyorsa queued kalır; done kararı yukarıda olabilir.
// Transfer listesinde eşleşme yok
// Done kararı için aşağıdaki !hasTransfers kontrolü beklenmeli
// Burada sadece "uploading" durumunu "queued"ye düşürüyoruz ki polling devam etsin
if (entry.moveStatus === "uploading") {
entry.moveStatus = "queued";
updated = true;
}
@@ -918,12 +925,20 @@ function updateMoveProgressFromStats(stats) {
applyProgress(job, prefixes, relRoot);
}
for (const job of mailruJobs.values()) {
const relRoot = job.folderId || "";
// MailRu için folderId null olabilir, fileName kullanıyoruz
const relRoot = job.folderId || job.fileName || "";
const prefixes = [
relRoot,
RCLONE_REMOTE_PATH ? `${RCLONE_REMOTE_PATH}/${relRoot}` : null
];
if (relRoot) {
// Debug: MailRu transfer eşleşmesi
const matched = transfers.filter((t) =>
prefixes.filter(Boolean).some((p) => String(t.name || "").includes(p))
);
if (job.moveStatus === "uploading" && matched.length === 0) {
console.log(`⚠️ MailRu transfer eşleşme yok: job=${job.fileName}, relRoot=${relRoot}, prefixes=${JSON.stringify(prefixes)}, transfers=${transfers.map(t => t.name).join(",")}`);
}
applyProgress(job, prefixes, relRoot);
}
}
@@ -1104,7 +1119,27 @@ async function runRcloneCacheClean() {
const body = await resp.text();
return { ok: false, error: `Rclone RC hata: ${body || resp.status}` };
}
return { ok: true, method: "rc", restarted: false };
// RC refresh sonrası cache dizinini temizle (mount düşmeden)
try {
if (fs.existsSync(RCLONE_VFS_CACHE_DIR)) {
const entries = fs.readdirSync(RCLONE_VFS_CACHE_DIR);
for (const entry of entries) {
const target = path.join(RCLONE_VFS_CACHE_DIR, entry);
fs.rmSync(target, { recursive: true, force: true });
}
} else {
fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true });
}
} catch (cleanupErr) {
return {
ok: false,
error: cleanupErr?.message || String(cleanupErr)
};
}
const remaining = fs.existsSync(RCLONE_VFS_CACHE_DIR)
? fs.readdirSync(RCLONE_VFS_CACHE_DIR).length
: 0;
return { ok: true, method: "rc+fs", restarted: false, remaining };
}
if (wasRunning && !RCLONE_RC_ENABLED) {
@@ -1114,7 +1149,8 @@ async function runRcloneCacheClean() {
// RC kapalıysa dosya sisteminden temizle
fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true });
fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true });
return { ok: true, method: "fs", restarted: false };
const remaining = fs.readdirSync(RCLONE_VFS_CACHE_DIR).length;
return { ok: true, method: "fs", restarted: false, remaining };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
@@ -1276,12 +1312,38 @@ function startRcloneMount(settings) {
rcloneProcess.stdout.on("data", (data) => {
const msg = data.toString().trim();
if (msg) console.log(`🌀 rclone: ${msg}`);
if (msg) {
rcloneLastLogMessage = msg;
// NOTICE mesajları için farklı ikon, diğerleri için normal
if (msg.toUpperCase().includes("NOTICE")) {
console.log(`📡 rclone: ${msg}`);
} else {
console.log(`🌀 rclone: ${msg}`);
}
}
});
rcloneProcess.stderr.on("data", (data) => {
const msg = data.toString().trim();
if (msg) {
rcloneLastError = msg;
if (msg.includes("Dir.Remove not empty")) {
console.info(` rclone: ${msg}`);
return;
}
rcloneLastLogMessage = msg;
// NOTICE ve INFO seviyesindeki loglar hata değil
// Sadece ERROR, FATAL, CRITICAL seviyesindekileri "son hata" olarak işaretle
const upperMsg = msg.toUpperCase();
if (upperMsg.includes("ERROR") ||
upperMsg.includes("FATAL") ||
upperMsg.includes("CRITICAL") ||
upperMsg.includes("FAILED") ||
upperMsg.includes("COULDN'T") ||
upperMsg.includes("CANNOT") ||
upperMsg.includes("REFUSED") ||
upperMsg.includes("TIMEOUT") ||
upperMsg.includes("CONNECTION")) {
rcloneLastError = msg;
}
console.warn(`⚠️ rclone: ${msg}`);
}
});
@@ -2064,7 +2126,8 @@ async function finalizeYoutubeJob(job, exitCode) {
startRcloneStatsPolling();
const moveResult = await moveRootFolderToGdrive(job.folderId);
if (moveResult.ok) {
// Upload tamamlanma durumu RC stats ile belirlenecek
job.moveStatus = "uploading";
scheduleSnapshotBroadcast();
} else {
job.moveStatus = "error";
job.moveError = moveResult.error || "GDrive taşıma hatası";
@@ -2410,7 +2473,8 @@ async function finalizeMailRuJob(job, exitCode) {
startRcloneStatsPolling();
const moveResult = await movePathToGdrive(relPath);
if (moveResult.ok) {
// Upload tamamlanma durumu RC stats ile belirlenecek
job.moveStatus = "uploading";
scheduleSnapshotBroadcast();
} else {
job.moveStatus = "error";
job.moveError = moveResult.error || "GDrive taşıma hatası";
@@ -5905,7 +5969,8 @@ function writeInfoForRoot(rootFolder, info) {
if (!rootFolder || !info) return;
const safe = sanitizeRelative(rootFolder);
if (!safe) return;
const target = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
const target = getInfoPathForRoot(safe);
if (!target) return;
try {
info.updatedAt = Date.now();
fs.writeFileSync(target, JSON.stringify(info, null, 2), "utf-8");
@@ -7256,7 +7321,8 @@ async function onTorrentDone({ torrent }) {
startRcloneStatsPolling();
const moveResult = await moveRootFolderToGdrive(rootFolder);
if (moveResult.ok) {
// Upload tamamlanma durumu RC stats ile belirlenecek
entry.moveStatus = "uploading";
scheduleSnapshotBroadcast();
} else {
entry.moveStatus = "error";
entry.moveError = moveResult.error || "GDrive taşıma hatası";
@@ -7744,17 +7810,58 @@ app.delete("/api/file", requireAuth, (req, res) => {
if (!filePath) return res.status(400).json({ error: "path gerekli" });
const safePath = sanitizeRelative(filePath);
const fullPath = path.join(DOWNLOAD_DIR, safePath);
let folderId = (safePath.split(/[\/]/)[0] || "").trim();
let rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
let folderIsDirectory = false;
if (rootDir && fs.existsSync(rootDir)) {
try {
folderIsDirectory = fs.statSync(rootDir).isDirectory();
} catch (err) {
folderIsDirectory = false;
// Dosyanın nerede olduğunu bul (DOWNLOAD_DIR veya GDRIVE_ROOT)
let fullPath = null;
let storageBase = null; // Dosyanın bulunduğu storage base (DOWNLOAD_DIR veya GDRIVE_ROOT)
for (const baseDir of getStorageRoots()) {
const testPath = path.join(baseDir, safePath);
if (fs.existsSync(testPath)) {
fullPath = testPath;
storageBase = baseDir;
break;
}
}
// Dosya bulunamadı
if (!fullPath) {
return res.status(404).json({ error: "Dosya bulunamadı" });
}
let folderId = (safePath.split(/[\/]/)[0] || "").trim();
let rootDir = null;
let folderIsDirectory = false;
// GDrive'da ise special handling - GDrive'ın kendisi root olarak kabul edilir
const isGDriveFile = storageBase === GDRIVE_ROOT;
if (isGDriveFile) {
// GDrive'da klasör yapısı farklıdır
// folderId varsa ve GDRIVE_ROOT/folderId bir klasörse
const testRootDir = path.join(GDRIVE_ROOT, folderId);
if (folderId && fs.existsSync(testRootDir)) {
try {
folderIsDirectory = fs.statSync(testRootDir).isDirectory();
if (folderIsDirectory) {
rootDir = GDRIVE_ROOT; // GDrive root'u
}
} catch (err) {
folderIsDirectory = false;
}
}
} else {
// Downloads klasörü için normal mantık
rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
if (rootDir && fs.existsSync(rootDir)) {
try {
folderIsDirectory = fs.statSync(rootDir).isDirectory();
} catch (err) {
folderIsDirectory = false;
}
}
}
// Kök dosyalarda ilk segment dosya adıdır; klasör değilse root davranışı uygula
if (folderId && !folderIsDirectory) {
folderId = "";
@@ -7783,6 +7890,31 @@ app.delete("/api/file", requireAuth, (req, res) => {
const isDirectory = stats.isDirectory();
const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/");
let trashEntry = null;
// GDrive dosyaları için özel handling - doğrudan sil
const isGDriveFile = storageBase === GDRIVE_ROOT;
if (isGDriveFile) {
// GDrive dosyaları için doğrudan silme (trash sistemi yok)
try {
if (isDirectory) {
fs.rmSync(fullPath, { recursive: true, force: true });
console.log(`🗑️ GDrive klasör silindi: ${safePath}`);
} else {
fs.unlinkSync(fullPath);
console.log(`🗑️ GDrive dosya silindi: ${safePath}`);
}
removeThumbnailsForPath(safePath);
broadcastFileUpdate("gdrive");
broadcastDiskSpace();
return res.json({ ok: true, filesRemoved: true, deletedFrom: "gdrive" });
} catch (deleteErr) {
console.error(`❌ GDrive dosya silme hatası: ${deleteErr.message}`);
return res.status(500).json({ error: `Silme hatası: ${deleteErr.message}` });
}
}
// Downloads klasörü için normal trash sistemi
if (folderId && folderIsDirectory && rootDir) {
const infoBeforeDelete = readInfoForRoot(folderId);
mediaFlags = detectMediaFlagsForPath(
@@ -8223,7 +8355,8 @@ app.get("/api/files", requireAuth, (req, res) => {
const seriesEpisodeInfo = relWithinRoot
? info.seriesEpisodes?.[relWithinRoot] || null
: null;
let mediaCategory = fileMeta?.type || null;
let mediaCategory =
fileMeta?.categoryOverride || fileMeta?.type || null;
if (!mediaCategory) {
const canInheritFromInfo = !relWithinRoot || isVideo;
if (canInheritFromInfo && info.type) {
@@ -8274,6 +8407,45 @@ app.get("/api/files", requireAuth, (req, res) => {
}
});
// --- 🏷️ Dosya kategori override ---
app.post("/api/file/category", requireAuth, (req, res) => {
try {
const relPath = sanitizeRelative(String(req.body?.path || ""));
const category = String(req.body?.category || "").toLowerCase();
if (!relPath) return res.status(400).json({ error: "Geçersiz yol" });
if (!category) return res.status(400).json({ error: "Geçersiz kategori" });
const rootFolder = rootFromRelPath(relPath);
if (!rootFolder) return res.status(400).json({ error: "Kök bulunamadı" });
const info = readInfoForRoot(rootFolder);
if (!info) return res.status(404).json({ error: "info.json bulunamadı" });
const segments = relPathToSegments(relPath);
const relWithinRoot = segments.slice(1).join("/");
if (!info.files) info.files = {};
if (!relWithinRoot) {
if (category === "auto") delete info.type;
else info.type = category;
writeInfoForRoot(rootFolder, info);
return res.json({ ok: true });
}
if (!info.files[relWithinRoot]) {
return res.status(404).json({ error: "Dosya kaydı bulunamadı" });
}
if (category === "auto") {
delete info.files[relWithinRoot].categoryOverride;
} else {
info.files[relWithinRoot].categoryOverride = category;
}
writeInfoForRoot(rootFolder, info);
return res.json({ ok: true });
} catch (err) {
console.error("❌ Kategori güncelleme hatası:", err);
res.status(500).json({ error: err.message });
}
});
// --- 🗑️ Çöp listesi API (.trash flag sistemi) ---
app.get("/api/trash", requireAuth, (req, res) => {
try {
@@ -9210,6 +9382,10 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
enabled: RCLONE_ENABLED,
mounted,
running: Boolean(rcloneProcess),
// Mount durumu hakkında daha fazla bilgi
mountStatus: !rcloneProcess ? "stopped" :
mounted ? "mounted" :
"starting", // Process çalışıyor ama mount henüz tamamlanmadı
mountDir: settings.mountDir,
remoteName: settings.remoteName,
remotePath: settings.remotePath,
@@ -9219,7 +9395,8 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
cacheCleanMinutes: settings.cacheCleanMinutes || 0,
configExists: fs.existsSync(settings.configPath),
remoteConfigured: rcloneConfigHasRemote(settings.remoteName),
lastError: rcloneLastError || null,
lastError: rcloneLastError || null, // Sadece gerçek hatalar
lastLog: rcloneLastLogMessage || null, // Son log mesajı (NOTICE dahil)
// Performans ayarları
vfsCacheMode: RCLONE_VFS_CACHE_MODE,
bufferSize: RCLONE_BUFFER_SIZE,
@@ -10237,18 +10414,25 @@ function collectMusicEntries() {
if (!fileKeys.length) continue;
let targetPath = info.primaryVideoPath;
if (targetPath && files[targetPath]?.type !== "music") {
const primaryMeta = targetPath ? files[targetPath] : null;
const primaryCategory =
primaryMeta?.categoryOverride || primaryMeta?.type || null;
if (targetPath && primaryCategory !== "music") {
targetPath = null;
}
if (!targetPath) {
targetPath =
fileKeys.find((key) => files[key]?.type === "music") || fileKeys[0];
fileKeys.find(
(key) =>
(files[key]?.categoryOverride || files[key]?.type) === "music"
) || fileKeys[0];
}
if (!targetPath) continue;
const fileMeta = files[targetPath];
// Hedef dosya çöpteyse atla
if (isPathTrashed(folder, targetPath, false)) continue;
const mediaType = fileMeta?.type || info.type || null;
const mediaType =
fileMeta?.categoryOverride || fileMeta?.type || info.type || null;
if (mediaType !== "music") continue;
const absMusic = resolveStoragePath(`${folder}/${targetPath}`);
if (!absMusic) continue;
@@ -10268,9 +10452,14 @@ function collectMusicEntries() {
const infoHash = info.infoHash || folder;
const title =
info.name || metadata?.title || path.basename(targetPath) || folder;
const thumbnail =
metadata?.thumbnail ||
(metadata ? `/yt-data/${folder}/thumbnail.jpg` : null);
// Thumbnail kontrolü - metadata varsa fiziksel dosya varlığını kontrol et
let thumbnail = metadata?.thumbnail;
if (!thumbnail && metadata) {
const thumbPath = path.join(YT_DATA_ROOT, folder, "thumbnail.jpg");
if (fs.existsSync(thumbPath)) {
thumbnail = `/yt-data/${folder}/thumbnail.jpg`;
}
}
entries.push({
id: `${folder}:${targetPath}`,
@@ -10557,7 +10746,12 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
const index = job.selectedIndex || 0;
const fileEntry = job.files[index] || job.files[0];
if (!fileEntry) return res.status(404).end();
const absPath = path.join(job.savePath, fileEntry.name);
const folderId = job.folderId || path.basename(job.savePath || "");
const relPath = folderId ? path.join(folderId, fileEntry.name) : fileEntry.name;
const absPath =
resolveStoragePath(relPath) ||
(job.savePath ? path.join(job.savePath, fileEntry.name) : null);
if (!absPath) return res.status(404).end();
return streamLocalFile(absPath, range, res);
}
@@ -10567,12 +10761,10 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
if (fileKeys.length) {
const idx = Number(req.query.index) || 0;
const targetKey = fileKeys[idx] || fileKeys[0];
const absPath = path.join(
DOWNLOAD_DIR,
req.params.hash,
targetKey.replace(/\\/g, "/")
);
if (fs.existsSync(absPath)) {
// Rclone ile taşınmış dosyalar için resolveStoragePath kullan
const relPath = path.join(req.params.hash, targetKey.replace(/\\/g, "/"));
const absPath = resolveStoragePath(relPath);
if (absPath) {
return streamLocalFile(absPath, range, res);
}
}