Compare commits
22 Commits
20da34beb2
...
rclone
| Author | SHA1 | Date | |
|---|---|---|---|
| 0255a44120 | |||
| 42447a63f3 | |||
| f9c0cc15f0 | |||
| 90f6291e14 | |||
| 7753b68578 | |||
| 6fc2d2c45f | |||
| e66ace9ed5 | |||
| 1774f681be | |||
| 2c06867c75 | |||
| 2d06056c1a | |||
| 92cdb4ee61 | |||
| d705e37d85 | |||
| 2f3dc72dcc | |||
| a011af7368 | |||
| 90587aa6d6 | |||
| c3d38d2e79 | |||
| 2b9c776c8a | |||
| 7269f52b0e | |||
| 1a7a8ec66e | |||
| 1b0662a5ec | |||
| 8825d0af8d | |||
| 44323275d8 |
@@ -97,3 +97,5 @@ RCLONE_AUTO_RESTART=1
|
|||||||
RCLONE_AUTO_RESTART_MAX_RETRIES=5
|
RCLONE_AUTO_RESTART_MAX_RETRIES=5
|
||||||
# Yeniden başlatma arasındaki bekleme süresi (milisaniye)
|
# Yeniden başlatma arasındaki bekleme süresi (milisaniye)
|
||||||
RCLONE_AUTO_RESTART_DELAY_MS=5000
|
RCLONE_AUTO_RESTART_DELAY_MS=5000
|
||||||
|
# Rclone settings dosya yolu (container içinde)
|
||||||
|
RCLONE_SETTINGS_PATH=/app/server/cache/rclone.json
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Router, Route } from "svelte-routing";
|
import { Router, Route } from "svelte-routing";
|
||||||
|
import { useLocation } from "svelte-routing";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Sidebar from "./components/Sidebar.svelte";
|
import Sidebar from "./components/Sidebar.svelte";
|
||||||
import Topbar from "./components/Topbar.svelte";
|
import Topbar from "./components/Topbar.svelte";
|
||||||
import MiniPlayer from "./components/MiniPlayer.svelte";
|
import MiniPlayer from "./components/MiniPlayer.svelte";
|
||||||
|
import Toast from "./components/Toast.svelte";
|
||||||
import Files from "./routes/Files.svelte";
|
import Files from "./routes/Files.svelte";
|
||||||
import Transfers from "./routes/Transfers.svelte";
|
import Transfers from "./routes/Transfers.svelte";
|
||||||
import Trash from "./routes/Trash.svelte";
|
import Trash from "./routes/Trash.svelte";
|
||||||
@@ -27,6 +29,8 @@
|
|||||||
let menuOpen = false;
|
let menuOpen = false;
|
||||||
let wsCounts;
|
let wsCounts;
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
|
const location = useLocation();
|
||||||
|
$: isMusicRoute = ($location?.pathname || "").startsWith("/music");
|
||||||
|
|
||||||
const scheduleMediaRefresh = () => {
|
const scheduleMediaRefresh = () => {
|
||||||
if (refreshTimer) return;
|
if (refreshTimer) return;
|
||||||
@@ -162,7 +166,10 @@
|
|||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MiniPlayer />
|
{#if !isMusicRoute}
|
||||||
|
<MiniPlayer />
|
||||||
|
{/if}
|
||||||
|
<Toast />
|
||||||
|
|
||||||
<!-- Sidebar dışına tıklayınca kapanma -->
|
<!-- Sidebar dışına tıklayınca kapanma -->
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { onMount, afterUpdate, tick } from "svelte";
|
||||||
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
|
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
|
||||||
|
import { useLocation } from "svelte-routing";
|
||||||
import { API, withToken } from "../utils/api.js";
|
import { API, withToken } from "../utils/api.js";
|
||||||
import { cleanFileName } from "../utils/filename.js";
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
|
||||||
let videoEl = null;
|
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) {
|
function thumbnailURL(item) {
|
||||||
if (!item?.thumbnail) return null;
|
if (!item?.thumbnail) return null;
|
||||||
@@ -36,39 +52,97 @@
|
|||||||
return withToken(`${API}/stream/${item.infoHash}?index=${index}`);
|
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) {
|
$: if (videoEl && $musicPlayer.currentTrack && $musicPlayer.isPlaying) {
|
||||||
const target = $musicPlayer.currentTime || 0;
|
const target = $musicPlayer.currentTime || 0;
|
||||||
if (Number.isFinite(target) && Math.abs(videoEl.currentTime - target) > 0.6) {
|
if (!Number.isFinite(target)) {
|
||||||
videoEl.currentTime = 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) {
|
$: if (videoEl && $musicPlayer.isPlaying) {
|
||||||
videoEl.play().catch(() => undefined);
|
videoEl.play().catch(() => undefined);
|
||||||
} else if (videoEl) {
|
} else if (videoEl) {
|
||||||
videoEl.pause();
|
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>
|
</script>
|
||||||
|
|
||||||
{#if $musicPlayer.currentTrack}
|
{#if $musicPlayer.currentTrack && !isMusicRoute}
|
||||||
<div class="mini-player" aria-label="Mini music player">
|
<div
|
||||||
<div class="mini-top">
|
class="mini-player {dragging ? 'dragging' : ''}"
|
||||||
<div class="mini-user-meta">
|
aria-label="Mini music player"
|
||||||
<div class="mini-user-name">
|
on:pointerdown={startDrag}
|
||||||
{cleanFileName($musicPlayer.currentTrack.title)}
|
style="transform: translate({dragX}px, {dragY}px);"
|
||||||
</div>
|
>
|
||||||
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
|
<div class="mini-top"></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>
|
|
||||||
|
|
||||||
<div class="mini-cover">
|
<div class="mini-cover">
|
||||||
{#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)}
|
{#if $musicPlayer.isPlaying && videoStreamURL($musicPlayer.currentTrack)}
|
||||||
@@ -90,6 +164,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<div class="mini-progress">
|
||||||
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
|
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
|
||||||
@@ -105,20 +193,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-controls">
|
<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>
|
<i class="fa-solid fa-backward-step"></i>
|
||||||
</button>
|
</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}
|
{#if $musicPlayer.isPlaying}
|
||||||
<i class="fa-solid fa-pause"></i>
|
<i class="fa-solid fa-pause"></i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa-solid fa-play"></i>
|
<i class="fa-solid fa-play"></i>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</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>
|
<i class="fa-solid fa-forward-step"></i>
|
||||||
</button>
|
</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>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,18 +227,21 @@
|
|||||||
color: #f6f6f6;
|
color: #f6f6f6;
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-player.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-top {
|
.mini-top {
|
||||||
display: flex;
|
min-height: 0;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-user-meta {
|
.mini-meta {
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
margin: 0 4px 8px -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-user-name {
|
.mini-user-name {
|
||||||
@@ -159,6 +250,19 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.mini-user-handle {
|
||||||
@@ -166,39 +270,55 @@
|
|||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-actions {
|
@keyframes mini-marquee {
|
||||||
display: flex;
|
0% {
|
||||||
gap: 8px;
|
transform: translateX(0);
|
||||||
}
|
opacity: 1;
|
||||||
|
}
|
||||||
.mini-icon-btn {
|
10% {
|
||||||
width: 32px;
|
transform: translateX(0);
|
||||||
height: 32px;
|
opacity: 1;
|
||||||
border-radius: 12px;
|
}
|
||||||
border: none;
|
60% {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
transform: translateX(calc(-1 * var(--marquee-shift)));
|
||||||
color: #f6f6f6;
|
opacity: 1;
|
||||||
cursor: pointer;
|
}
|
||||||
|
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 {
|
.mini-cover {
|
||||||
margin: 14px auto 12px;
|
margin: -16px -16px 12px;
|
||||||
width: 160px;
|
width: calc(100% + 32px);
|
||||||
height: 160px;
|
aspect-ratio: 1 / 1;
|
||||||
border-radius: 20px;
|
border-radius: 26px 26px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: none;
|
||||||
display: grid;
|
display: block;
|
||||||
place-items: center;
|
|
||||||
transform: translateY(-6px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-cover video,
|
||||||
.mini-cover img {
|
.mini-cover img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-cover video {
|
.mini-cover video {
|
||||||
|
|||||||
76
client/src/components/Toast.svelte
Normal file
76
client/src/components/Toast.svelte
Normal 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>
|
||||||
@@ -626,6 +626,25 @@
|
|||||||
alert(err?.message || "GDrive taşıma başarısız oldu.");
|
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) {
|
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";
|
||||||
@@ -1284,10 +1303,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);
|
||||||
@@ -1305,45 +1320,55 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchFile(file) {
|
function matchFile(file) {
|
||||||
if (!file || file.isDirectory) {
|
try {
|
||||||
closeMenu();
|
if (!file || file.isDirectory) {
|
||||||
return;
|
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 = "";
|
|
||||||
}
|
}
|
||||||
|
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() {
|
function closeMatchModal() {
|
||||||
@@ -1374,6 +1399,10 @@
|
|||||||
params.set("year", matchYear);
|
params.set("year", matchYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
params.set("token", token);
|
||||||
|
}
|
||||||
const response = await apiFetch(`/api/search/metadata?${params}`);
|
const response = await apiFetch(`/api/search/metadata?${params}`);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -1490,8 +1519,6 @@
|
|||||||
function closeMenu() {
|
function closeMenu() {
|
||||||
activeMenu = null;
|
activeMenu = null;
|
||||||
deleteConfirmPending = false;
|
deleteConfirmPending = false;
|
||||||
showMatchModal = false;
|
|
||||||
matchingFile = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klasör oluşturma fonksiyonları
|
// Klasör oluşturma fonksiyonları
|
||||||
@@ -2399,6 +2426,20 @@
|
|||||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||||
<span>Eşleştir</span>
|
<span>Eşleştir</span>
|
||||||
</button>
|
</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>
|
<div class="menu-divider"></div>
|
||||||
<button
|
<button
|
||||||
class="menu-item"
|
class="menu-item"
|
||||||
@@ -3251,12 +3292,14 @@
|
|||||||
.name {
|
.name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
max-height: calc(1.3em * 2);
|
max-height: calc(1.3em * 2);
|
||||||
|
min-height: calc(1.3em * 2);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -3827,7 +3870,8 @@
|
|||||||
.folder-info {
|
.folder-info {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
|
padding: 0 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.folder-name {
|
.folder-name {
|
||||||
@@ -3839,8 +3883,9 @@
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
max-height: calc(1.25em * 2);
|
max-height: calc(1.25em * 2);
|
||||||
|
min-height: calc(1.25em * 2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { apiFetch } from "../utils/api.js";
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
import { showToast } from "../stores/toastStore.js";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "general", label: "General", icon: "fa-solid fa-sliders" },
|
{ id: "general", label: "General", icon: "sliders" },
|
||||||
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" },
|
{ id: "youtube", label: "YouTube", icon: "youtube" },
|
||||||
{ id: "rclone", label: "Rclone", icon: "fa-solid fa-cloud" },
|
{ id: "rclone", label: "Rclone", icon: "cloud" },
|
||||||
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
|
{ id: "advanced", label: "Advanced", icon: "gear" }
|
||||||
];
|
];
|
||||||
|
|
||||||
let activeTab = "youtube";
|
let activeTab = "youtube";
|
||||||
@@ -20,7 +21,6 @@
|
|||||||
let onlyAudio = false;
|
let onlyAudio = false;
|
||||||
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
|
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
|
||||||
let error = null;
|
let error = null;
|
||||||
let success = null;
|
|
||||||
|
|
||||||
let rcloneStatus = null;
|
let rcloneStatus = null;
|
||||||
let rcloneLoading = false;
|
let rcloneLoading = false;
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
async function loadCookies() {
|
async function loadCookies() {
|
||||||
loadingCookies = true;
|
loadingCookies = true;
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/youtube/cookies");
|
const resp = await apiFetch("/api/youtube/cookies");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
@@ -51,7 +50,6 @@
|
|||||||
async function saveCookies() {
|
async function saveCookies() {
|
||||||
if (savingCookies) return;
|
if (savingCookies) return;
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
savingCookies = true;
|
savingCookies = true;
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -71,7 +69,7 @@
|
|||||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
cookiesUpdatedAt = data.updatedAt || Date.now();
|
cookiesUpdatedAt = data.updatedAt || Date.now();
|
||||||
success = "Cookies kaydedildi.";
|
showToast("Cookies kaydedildi.", "success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Cookies kaydedilemedi.";
|
error = err?.message || "Cookies kaydedilemedi.";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -99,7 +97,6 @@
|
|||||||
if (savingYtSettings) return;
|
if (savingYtSettings) return;
|
||||||
savingYtSettings = true;
|
savingYtSettings = true;
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/youtube/settings", {
|
const resp = await apiFetch("/api/youtube/settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -113,7 +110,7 @@
|
|||||||
if (!resp.ok || !data?.ok) {
|
if (!resp.ok || !data?.ok) {
|
||||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
success = "YouTube indirme ayarları kaydedildi.";
|
showToast("YouTube indirme ayarları kaydedildi.", "success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "YouTube ayarları kaydedilemedi.";
|
error = err?.message || "YouTube ayarları kaydedilemedi.";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -156,7 +153,6 @@
|
|||||||
if (rcloneSaving) return;
|
if (rcloneSaving) return;
|
||||||
rcloneSaving = true;
|
rcloneSaving = true;
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/rclone/settings", {
|
const resp = await apiFetch("/api/rclone/settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -180,7 +176,7 @@
|
|||||||
if (!confResp.ok || !confData?.ok) {
|
if (!confResp.ok || !confData?.ok) {
|
||||||
throw new Error(confData?.error || `HTTP ${confResp.status}`);
|
throw new Error(confData?.error || `HTTP ${confResp.status}`);
|
||||||
}
|
}
|
||||||
success = "Rclone ayarları kaydedildi.";
|
showToast("Rclone ayarları kaydedildi.", "success");
|
||||||
await loadRcloneStatus();
|
await loadRcloneStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Rclone ayarları kaydedilemedi.";
|
error = err?.message || "Rclone ayarları kaydedilemedi.";
|
||||||
@@ -191,7 +187,6 @@
|
|||||||
|
|
||||||
async function startRcloneMount() {
|
async function startRcloneMount() {
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/rclone/mount", { method: "POST" });
|
const resp = await apiFetch("/api/rclone/mount", { method: "POST" });
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
@@ -200,16 +195,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
|
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
|
||||||
success = "Rclone mount başlatılıyor...";
|
showToast("Rclone mount başlatılıyor...", "info");
|
||||||
|
|
||||||
// 2 saniye sonra status güncelle
|
// 2 saniye sonra status güncelle
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await loadRcloneStatus();
|
await loadRcloneStatus();
|
||||||
// Status yüklendikten sonra mesajı güncelle
|
// Status yüklendikten sonra mesajı güncelle
|
||||||
if (rcloneStatus?.mounted) {
|
if (rcloneStatus?.mounted) {
|
||||||
success = "Rclone mount başarıyla başlatıldı.";
|
showToast("Rclone mount başarıyla başlatıldı.", "success");
|
||||||
} else if (rcloneStatus?.running) {
|
} else if (rcloneStatus?.running) {
|
||||||
success = "Rclone mount başlatıldı, mount tamamlanıyor...";
|
showToast("Rclone mount başlatıldı, mount tamamlanıyor...", "info");
|
||||||
} else {
|
} else {
|
||||||
error = "Rclone mount başlatılamadı.";
|
error = "Rclone mount başlatılamadı.";
|
||||||
}
|
}
|
||||||
@@ -224,14 +219,13 @@
|
|||||||
|
|
||||||
async function stopRcloneMount() {
|
async function stopRcloneMount() {
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/rclone/unmount", { method: "POST" });
|
const resp = await apiFetch("/api/rclone/unmount", { method: "POST" });
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !data?.ok) {
|
if (!resp.ok || !data?.ok) {
|
||||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
success = "Rclone mount durduruldu.";
|
showToast("Rclone mount durduruldu.", "success");
|
||||||
await loadRcloneStatus();
|
await loadRcloneStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Rclone mount durdurulamadı.";
|
error = err?.message || "Rclone mount durdurulamadı.";
|
||||||
@@ -240,35 +234,17 @@
|
|||||||
|
|
||||||
async function cleanRcloneCache() {
|
async function cleanRcloneCache() {
|
||||||
error = null;
|
error = null;
|
||||||
success = null;
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" });
|
const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" });
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !data?.ok) {
|
if (!resp.ok || !data?.ok) {
|
||||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
success = "Rclone cache temizlendi.";
|
showToast("Cache temizlendi.", "success");
|
||||||
|
await loadRcloneStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Rclone cache temizlenemedi.";
|
error = err?.message || "Cache temizlenemedi.";
|
||||||
}
|
showToast(error, "error");
|
||||||
}
|
|
||||||
|
|
||||||
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.";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,17 +271,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
{#each tabs as tab}
|
<button
|
||||||
<button
|
class="tab {activeTab === 'general' ? 'active' : ''}"
|
||||||
class:active={activeTab === tab.id}
|
type="button"
|
||||||
class="tab"
|
on:click={() => activeTab = 'general'}
|
||||||
type="button"
|
>
|
||||||
on:click={() => (activeTab = tab.id)}
|
<i class="fa-solid fa-sliders"></i>
|
||||||
>
|
<span>General</span>
|
||||||
<i class={tab.icon}></i>
|
</button>
|
||||||
<span>{tab.label}</span>
|
<button
|
||||||
</button>
|
class="tab {activeTab === 'youtube' ? 'active' : ''}"
|
||||||
{/each}
|
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>
|
</div>
|
||||||
|
|
||||||
{#if activeTab === "youtube"}
|
{#if activeTab === "youtube"}
|
||||||
@@ -390,12 +387,6 @@
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if success}
|
|
||||||
<div class="alert success">
|
|
||||||
<i class="fa-solid fa-circle-check"></i>
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</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>
|
||||||
@@ -439,11 +430,8 @@
|
|||||||
disabled={rcloneLoading || rcloneSaving}
|
disabled={rcloneLoading || rcloneSaving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" on:click={cleanRcloneCache}>
|
<button class="btn primary" on:click={cleanRcloneCache}>
|
||||||
<i class="fa-solid fa-broom"></i> Temizle
|
<i class="fa-solid fa-broom"></i> Cache Temizle
|
||||||
</button>
|
|
||||||
<button class="btn primary" on:click={checkAndCleanCache}>
|
|
||||||
<i class="fa-solid fa-wand-magic-sparkles"></i> Akıllı Temizle
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -472,7 +460,24 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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}>
|
<button class="btn" on:click={startRcloneMount}>
|
||||||
<i class="fa-solid fa-play"></i> Mount Başlat
|
<i class="fa-solid fa-play"></i> Mount Başlat
|
||||||
</button>
|
</button>
|
||||||
@@ -482,7 +487,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if rcloneStatus}
|
{#if rcloneStatus}
|
||||||
<div class="card muted" style="margin-top:10px;">
|
<div class="card muted" style="margin-top:12px;">
|
||||||
<div><strong>Durum:</strong></div>
|
<div><strong>Durum:</strong></div>
|
||||||
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
|
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -527,19 +532,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
</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>
|
||||||
|
|||||||
24
client/src/stores/toastStore.js
Normal file
24
client/src/stores/toastStore.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const toast = writable({
|
||||||
|
message: null,
|
||||||
|
type: 'success', // success, error, info
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
let toastTimeout = null;
|
||||||
|
|
||||||
|
export function showToast(message, type = 'success', duration = 3000) {
|
||||||
|
// Önceki toast'ı temizle
|
||||||
|
if (toastTimeout) {
|
||||||
|
clearTimeout(toastTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yeni toast'ı göster
|
||||||
|
toast.set({ message, type, visible: true });
|
||||||
|
|
||||||
|
// Belirli süre sonra gizle
|
||||||
|
toastTimeout = setTimeout(() => {
|
||||||
|
toast.set({ message: null, type: 'success', visible: false });
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
@@ -56,3 +56,4 @@ services:
|
|||||||
RCLONE_AUTO_RESTART: ${RCLONE_AUTO_RESTART}
|
RCLONE_AUTO_RESTART: ${RCLONE_AUTO_RESTART}
|
||||||
RCLONE_AUTO_RESTART_MAX_RETRIES: ${RCLONE_AUTO_RESTART_MAX_RETRIES}
|
RCLONE_AUTO_RESTART_MAX_RETRIES: ${RCLONE_AUTO_RESTART_MAX_RETRIES}
|
||||||
RCLONE_AUTO_RESTART_DELAY_MS: ${RCLONE_AUTO_RESTART_DELAY_MS}
|
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": {}
|
||||||
|
}
|
||||||
158
server/server.js
158
server/server.js
@@ -186,7 +186,7 @@ const FFPROBE_MAX_BUFFER =
|
|||||||
Number(process.env.FFPROBE_MAX_BUFFER) > 0
|
Number(process.env.FFPROBE_MAX_BUFFER) > 0
|
||||||
? Number(process.env.FFPROBE_MAX_BUFFER)
|
? Number(process.env.FFPROBE_MAX_BUFFER)
|
||||||
: 10 * 1024 * 1024;
|
: 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() {
|
function getWsClientCount() {
|
||||||
if (!wss) return 0;
|
if (!wss) return 0;
|
||||||
@@ -723,6 +723,16 @@ function readInfoForRoot(rootFolder) {
|
|||||||
return null;
|
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) {
|
function sanitizeRelative(relPath) {
|
||||||
return relPath.replace(/^[\\/]+/, "");
|
return relPath.replace(/^[\\/]+/, "");
|
||||||
}
|
}
|
||||||
@@ -888,14 +898,10 @@ function updateMoveProgressFromStats(stats) {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const gdriveTarget = relRoot ? path.join(GDRIVE_ROOT, relRoot) : null;
|
// Transfer listesinde eşleşme yok
|
||||||
const targetExists = gdriveTarget ? fs.existsSync(gdriveTarget) : false;
|
// Done kararı için aşağıdaki !hasTransfers kontrolü beklenmeli
|
||||||
if (targetExists) {
|
// Burada sadece "uploading" durumunu "queued"ye düşürüyoruz ki polling devam etsin
|
||||||
entry.moveStatus = "done";
|
if (entry.moveStatus === "uploading") {
|
||||||
entry.moveProgress = 1;
|
|
||||||
updated = true;
|
|
||||||
} else if (entry.moveStatus === "uploading") {
|
|
||||||
// Transfer görünmüyorsa queued kalır; done kararı yukarıda olabilir.
|
|
||||||
entry.moveStatus = "queued";
|
entry.moveStatus = "queued";
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
@@ -919,12 +925,20 @@ function updateMoveProgressFromStats(stats) {
|
|||||||
applyProgress(job, prefixes, relRoot);
|
applyProgress(job, prefixes, relRoot);
|
||||||
}
|
}
|
||||||
for (const job of mailruJobs.values()) {
|
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 = [
|
const prefixes = [
|
||||||
relRoot,
|
relRoot,
|
||||||
RCLONE_REMOTE_PATH ? `${RCLONE_REMOTE_PATH}/${relRoot}` : null
|
RCLONE_REMOTE_PATH ? `${RCLONE_REMOTE_PATH}/${relRoot}` : null
|
||||||
];
|
];
|
||||||
if (relRoot) {
|
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);
|
applyProgress(job, prefixes, relRoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1105,7 +1119,27 @@ async function runRcloneCacheClean() {
|
|||||||
const body = await resp.text();
|
const body = await resp.text();
|
||||||
return { ok: false, error: `Rclone RC hata: ${body || resp.status}` };
|
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) {
|
if (wasRunning && !RCLONE_RC_ENABLED) {
|
||||||
@@ -1115,7 +1149,8 @@ async function runRcloneCacheClean() {
|
|||||||
// RC kapalıysa dosya sisteminden temizle
|
// RC kapalıysa dosya sisteminden temizle
|
||||||
fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true });
|
fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true });
|
||||||
fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: 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) {
|
} catch (err) {
|
||||||
return { ok: false, error: err?.message || String(err) };
|
return { ok: false, error: err?.message || String(err) };
|
||||||
}
|
}
|
||||||
@@ -1290,6 +1325,10 @@ function startRcloneMount(settings) {
|
|||||||
rcloneProcess.stderr.on("data", (data) => {
|
rcloneProcess.stderr.on("data", (data) => {
|
||||||
const msg = data.toString().trim();
|
const msg = data.toString().trim();
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
if (msg.includes("Dir.Remove not empty")) {
|
||||||
|
console.info(`ℹ️ rclone: ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
rcloneLastLogMessage = msg;
|
rcloneLastLogMessage = msg;
|
||||||
// NOTICE ve INFO seviyesindeki loglar hata değil
|
// NOTICE ve INFO seviyesindeki loglar hata değil
|
||||||
// Sadece ERROR, FATAL, CRITICAL seviyesindekileri "son hata" olarak işaretle
|
// Sadece ERROR, FATAL, CRITICAL seviyesindekileri "son hata" olarak işaretle
|
||||||
@@ -2087,7 +2126,8 @@ async function finalizeYoutubeJob(job, exitCode) {
|
|||||||
startRcloneStatsPolling();
|
startRcloneStatsPolling();
|
||||||
const moveResult = await moveRootFolderToGdrive(job.folderId);
|
const moveResult = await moveRootFolderToGdrive(job.folderId);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
// Upload tamamlanma durumu RC stats ile belirlenecek
|
job.moveStatus = "uploading";
|
||||||
|
scheduleSnapshotBroadcast();
|
||||||
} else {
|
} else {
|
||||||
job.moveStatus = "error";
|
job.moveStatus = "error";
|
||||||
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
@@ -2433,7 +2473,8 @@ async function finalizeMailRuJob(job, exitCode) {
|
|||||||
startRcloneStatsPolling();
|
startRcloneStatsPolling();
|
||||||
const moveResult = await movePathToGdrive(relPath);
|
const moveResult = await movePathToGdrive(relPath);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
// Upload tamamlanma durumu RC stats ile belirlenecek
|
job.moveStatus = "uploading";
|
||||||
|
scheduleSnapshotBroadcast();
|
||||||
} else {
|
} else {
|
||||||
job.moveStatus = "error";
|
job.moveStatus = "error";
|
||||||
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
@@ -5928,7 +5969,8 @@ function writeInfoForRoot(rootFolder, info) {
|
|||||||
if (!rootFolder || !info) return;
|
if (!rootFolder || !info) return;
|
||||||
const safe = sanitizeRelative(rootFolder);
|
const safe = sanitizeRelative(rootFolder);
|
||||||
if (!safe) return;
|
if (!safe) return;
|
||||||
const target = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
|
const target = getInfoPathForRoot(safe);
|
||||||
|
if (!target) return;
|
||||||
try {
|
try {
|
||||||
info.updatedAt = Date.now();
|
info.updatedAt = Date.now();
|
||||||
fs.writeFileSync(target, JSON.stringify(info, null, 2), "utf-8");
|
fs.writeFileSync(target, JSON.stringify(info, null, 2), "utf-8");
|
||||||
@@ -7279,7 +7321,8 @@ async function onTorrentDone({ torrent }) {
|
|||||||
startRcloneStatsPolling();
|
startRcloneStatsPolling();
|
||||||
const moveResult = await moveRootFolderToGdrive(rootFolder);
|
const moveResult = await moveRootFolderToGdrive(rootFolder);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
// Upload tamamlanma durumu RC stats ile belirlenecek
|
entry.moveStatus = "uploading";
|
||||||
|
scheduleSnapshotBroadcast();
|
||||||
} else {
|
} else {
|
||||||
entry.moveStatus = "error";
|
entry.moveStatus = "error";
|
||||||
entry.moveError = moveResult.error || "GDrive taşıma hatası";
|
entry.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
@@ -8312,7 +8355,8 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
const seriesEpisodeInfo = relWithinRoot
|
const seriesEpisodeInfo = relWithinRoot
|
||||||
? info.seriesEpisodes?.[relWithinRoot] || null
|
? info.seriesEpisodes?.[relWithinRoot] || null
|
||||||
: null;
|
: null;
|
||||||
let mediaCategory = fileMeta?.type || null;
|
let mediaCategory =
|
||||||
|
fileMeta?.categoryOverride || fileMeta?.type || null;
|
||||||
if (!mediaCategory) {
|
if (!mediaCategory) {
|
||||||
const canInheritFromInfo = !relWithinRoot || isVideo;
|
const canInheritFromInfo = !relWithinRoot || isVideo;
|
||||||
if (canInheritFromInfo && info.type) {
|
if (canInheritFromInfo && info.type) {
|
||||||
@@ -8363,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) ---
|
// --- 🗑️ Çöp listesi API (.trash flag sistemi) ---
|
||||||
app.get("/api/trash", requireAuth, (req, res) => {
|
app.get("/api/trash", requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -10331,18 +10414,25 @@ function collectMusicEntries() {
|
|||||||
if (!fileKeys.length) continue;
|
if (!fileKeys.length) continue;
|
||||||
|
|
||||||
let targetPath = info.primaryVideoPath;
|
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;
|
targetPath = null;
|
||||||
}
|
}
|
||||||
if (!targetPath) {
|
if (!targetPath) {
|
||||||
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;
|
if (!targetPath) continue;
|
||||||
const fileMeta = files[targetPath];
|
const fileMeta = files[targetPath];
|
||||||
// Hedef dosya çöpteyse atla
|
// Hedef dosya çöpteyse atla
|
||||||
if (isPathTrashed(folder, targetPath, false)) continue;
|
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;
|
if (mediaType !== "music") continue;
|
||||||
const absMusic = resolveStoragePath(`${folder}/${targetPath}`);
|
const absMusic = resolveStoragePath(`${folder}/${targetPath}`);
|
||||||
if (!absMusic) continue;
|
if (!absMusic) continue;
|
||||||
@@ -10362,9 +10452,14 @@ function collectMusicEntries() {
|
|||||||
const infoHash = info.infoHash || folder;
|
const infoHash = info.infoHash || folder;
|
||||||
const title =
|
const title =
|
||||||
info.name || metadata?.title || path.basename(targetPath) || folder;
|
info.name || metadata?.title || path.basename(targetPath) || folder;
|
||||||
const thumbnail =
|
// Thumbnail kontrolü - metadata varsa fiziksel dosya varlığını kontrol et
|
||||||
metadata?.thumbnail ||
|
let thumbnail = metadata?.thumbnail;
|
||||||
(metadata ? `/yt-data/${folder}/thumbnail.jpg` : null);
|
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({
|
entries.push({
|
||||||
id: `${folder}:${targetPath}`,
|
id: `${folder}:${targetPath}`,
|
||||||
@@ -10651,7 +10746,12 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
|
|||||||
const index = job.selectedIndex || 0;
|
const index = job.selectedIndex || 0;
|
||||||
const fileEntry = job.files[index] || job.files[0];
|
const fileEntry = job.files[index] || job.files[0];
|
||||||
if (!fileEntry) return res.status(404).end();
|
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);
|
return streamLocalFile(absPath, range, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10661,12 +10761,10 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
|
|||||||
if (fileKeys.length) {
|
if (fileKeys.length) {
|
||||||
const idx = Number(req.query.index) || 0;
|
const idx = Number(req.query.index) || 0;
|
||||||
const targetKey = fileKeys[idx] || fileKeys[0];
|
const targetKey = fileKeys[idx] || fileKeys[0];
|
||||||
const absPath = path.join(
|
// Rclone ile taşınmış dosyalar için resolveStoragePath kullan
|
||||||
DOWNLOAD_DIR,
|
const relPath = path.join(req.params.hash, targetKey.replace(/\\/g, "/"));
|
||||||
req.params.hash,
|
const absPath = resolveStoragePath(relPath);
|
||||||
targetKey.replace(/\\/g, "/")
|
if (absPath) {
|
||||||
);
|
|
||||||
if (fs.existsSync(absPath)) {
|
|
||||||
return streamLocalFile(absPath, range, res);
|
return streamLocalFile(absPath, range, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user