Sİdebar'da music kategorisi oluşturuldu
This commit is contained in:
@@ -8,10 +8,12 @@
|
|||||||
import Trash from "./routes/Trash.svelte";
|
import Trash from "./routes/Trash.svelte";
|
||||||
import Movies from "./routes/Movies.svelte";
|
import Movies from "./routes/Movies.svelte";
|
||||||
import TvShows from "./routes/TvShows.svelte";
|
import TvShows from "./routes/TvShows.svelte";
|
||||||
|
import Music from "./routes/Music.svelte";
|
||||||
import Login from "./routes/Login.svelte";
|
import Login from "./routes/Login.svelte";
|
||||||
import { API, getAccessToken } from "./utils/api.js";
|
import { API, getAccessToken } from "./utils/api.js";
|
||||||
import { refreshMovieCount } from "./stores/movieStore.js";
|
import { refreshMovieCount } from "./stores/movieStore.js";
|
||||||
import { refreshTvShowCount } from "./stores/tvStore.js";
|
import { refreshTvShowCount } from "./stores/tvStore.js";
|
||||||
|
import { refreshMusicCount } from "./stores/musicStore.js";
|
||||||
import { fetchTrashItems } from "./stores/trashStore.js";
|
import { fetchTrashItems } from "./stores/trashStore.js";
|
||||||
|
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
@@ -25,7 +27,12 @@
|
|||||||
refreshTimer = setTimeout(async () => {
|
refreshTimer = setTimeout(async () => {
|
||||||
refreshTimer = null;
|
refreshTimer = null;
|
||||||
try {
|
try {
|
||||||
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
|
await Promise.all([
|
||||||
|
refreshMovieCount(),
|
||||||
|
refreshTvShowCount(),
|
||||||
|
refreshMusicCount(),
|
||||||
|
fetchTrashItems()
|
||||||
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Medya sayacı yenileme başarısız:", err);
|
console.warn("Medya sayacı yenileme başarısız:", err);
|
||||||
}
|
}
|
||||||
@@ -46,6 +53,7 @@
|
|||||||
if (token) {
|
if (token) {
|
||||||
refreshMovieCount();
|
refreshMovieCount();
|
||||||
refreshTvShowCount();
|
refreshTvShowCount();
|
||||||
|
refreshMusicCount();
|
||||||
fetchTrashItems();
|
fetchTrashItems();
|
||||||
const authToken = getAccessToken();
|
const authToken = getAccessToken();
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
@@ -60,7 +68,11 @@
|
|||||||
} else if (
|
} else if (
|
||||||
msg.type === "progress" &&
|
msg.type === "progress" &&
|
||||||
Array.isArray(msg.torrents) &&
|
Array.isArray(msg.torrents) &&
|
||||||
msg.torrents.some((t) => Number(t.progress) >= 1)
|
msg.torrents.some(
|
||||||
|
(t) =>
|
||||||
|
Number(t.progress) >= 1 ||
|
||||||
|
(t.type && String(t.type).toLowerCase() === "youtube")
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
scheduleMediaRefresh();
|
scheduleMediaRefresh();
|
||||||
}
|
}
|
||||||
@@ -105,6 +117,7 @@
|
|||||||
<Route path="/files" component={Files} />
|
<Route path="/files" component={Files} />
|
||||||
<Route path="/movies" component={Movies} />
|
<Route path="/movies" component={Movies} />
|
||||||
<Route path="/tv" component={TvShows} />
|
<Route path="/tv" component={TvShows} />
|
||||||
|
<Route path="/music" component={Music} />
|
||||||
<Route path="/transfers" component={Transfers} />
|
<Route path="/transfers" component={Transfers} />
|
||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Link } from "svelte-routing";
|
import { Link } from "svelte-routing";
|
||||||
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
||||||
import { movieCount } from "../stores/movieStore.js";
|
import { movieCount } from "../stores/movieStore.js";
|
||||||
import { tvShowCount } from "../stores/tvStore.js";
|
import { tvShowCount } from "../stores/tvStore.js";
|
||||||
|
import { musicCount } from "../stores/musicStore.js";
|
||||||
import { trashCount } from "../stores/trashStore.js";
|
import { trashCount } from "../stores/trashStore.js";
|
||||||
import { apiFetch } from "../utils/api.js";
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
@@ -10,7 +11,8 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let hasMovies = false;
|
let hasMovies = false;
|
||||||
let hasShows = false;
|
let hasShows = false;
|
||||||
let hasTrash = false;
|
let hasTrash = false;
|
||||||
|
let hasMusic = false;
|
||||||
// Svelte store kullanarak reaktivite sağla
|
// Svelte store kullanarak reaktivite sağla
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
|
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
|
||||||
@@ -39,14 +41,19 @@
|
|||||||
hasShows = (count ?? 0) > 0;
|
hasShows = (count ?? 0) > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeTrash = trashCount.subscribe((count) => {
|
const unsubscribeTrash = trashCount.subscribe((count) => {
|
||||||
hasTrash = (count ?? 0) > 0;
|
hasTrash = (count ?? 0) > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unsubscribeMusic = musicCount.subscribe((count) => {
|
||||||
|
hasMusic = (count ?? 0) > 0;
|
||||||
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
unsubscribeMovie();
|
unsubscribeMovie();
|
||||||
unsubscribeTv();
|
unsubscribeTv();
|
||||||
unsubscribeTrash();
|
unsubscribeTrash();
|
||||||
|
unsubscribeMusic();
|
||||||
if (unsubscribeDiskSpace) {
|
if (unsubscribeDiskSpace) {
|
||||||
unsubscribeDiskSpace();
|
unsubscribeDiskSpace();
|
||||||
}
|
}
|
||||||
@@ -167,6 +174,20 @@
|
|||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if hasMusic}
|
||||||
|
<Link
|
||||||
|
to="/music"
|
||||||
|
class="item"
|
||||||
|
getProps={({ isCurrent }) => ({
|
||||||
|
class: isCurrent ? "item active" : "item",
|
||||||
|
})}
|
||||||
|
on:click={handleLinkClick}
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-music icon"></i>
|
||||||
|
Music
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/transfers"
|
to="/transfers"
|
||||||
class="item"
|
class="item"
|
||||||
|
|||||||
284
client/src/routes/Music.svelte
Normal file
284
client/src/routes/Music.svelte
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { API, apiFetch, withToken } from "../utils/api.js";
|
||||||
|
import { musicCount } from "../stores/musicStore.js";
|
||||||
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
let loading = true;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
async function loadMusic() {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
try {
|
||||||
|
const resp = await apiFetch("/api/music");
|
||||||
|
if (!resp.ok) {
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
const list = await resp.json();
|
||||||
|
items = Array.isArray(list) ? list : [];
|
||||||
|
musicCount.set(items.length);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Music load error:", err);
|
||||||
|
items = [];
|
||||||
|
musicCount.set(0);
|
||||||
|
error = err?.message || "Music listesi yüklenemedi.";
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamURL(item) {
|
||||||
|
const base = `${API}/stream/${item.infoHash}?index=${item.fileIndex || 0}`;
|
||||||
|
return withToken(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailURL(item) {
|
||||||
|
if (!item.thumbnail) return null;
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const separator = item.thumbnail.includes("?") ? "&" : "?";
|
||||||
|
return `${API}${item.thumbnail}${separator}token=${token}&v=${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
if (!Number.isFinite(seconds) || seconds <= 0) return "";
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(item) {
|
||||||
|
if (item.tracker === "youtube" || item.thumbnail) return "YouTube";
|
||||||
|
return "Music";
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadMusic();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="music-page">
|
||||||
|
<div class="music-header">
|
||||||
|
<div class="music-title-wrap">
|
||||||
|
<h2>Music</h2>
|
||||||
|
{#if !loading && !error}
|
||||||
|
<span class="song-count">{items.length} Songs</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<button class="refresh-btn" on:click={loadMusic} disabled={loading}>
|
||||||
|
<i class="fa-solid fa-rotate"></i>
|
||||||
|
Yenile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="music-empty">Yükleniyor…</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="music-empty error">{error}</div>
|
||||||
|
{:else if !items.length}
|
||||||
|
<div class="music-empty">Henüz müzik videosu yok.</div>
|
||||||
|
{:else}
|
||||||
|
<div class="music-list">
|
||||||
|
{#each items as item, idx (item.id)}
|
||||||
|
<div class="music-row">
|
||||||
|
<div class="index">{String(idx + 1).padStart(2, "0")}</div>
|
||||||
|
<div class="thumb">
|
||||||
|
{#if thumbnailURL(item)}
|
||||||
|
<img src={thumbnailURL(item)} alt={item.title} />
|
||||||
|
{:else}
|
||||||
|
<div class="thumb-placeholder">
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="name">{cleanFileName(item.title)}</div>
|
||||||
|
<div class="meta">{sourceLabel(item)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="duration">
|
||||||
|
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a
|
||||||
|
class="play-btn"
|
||||||
|
href={streamURL(item)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="Oynat"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="open-btn"
|
||||||
|
href={`/files?path=${encodeURIComponent(item.folder)}`}
|
||||||
|
title="Klasöre git"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-folder-open"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.music-page {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-count {
|
||||||
|
color: #8a8fa3;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 56px 1fr 70px 90px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8a8fa3;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #d2d8ff, #f0f3ff);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-placeholder {
|
||||||
|
color: #6f7ba3;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info .name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1c2440;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info .meta {
|
||||||
|
color: #8a8fa3;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
text-align: right;
|
||||||
|
color: #8a8fa3;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn,
|
||||||
|
.open-btn {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #1f8a70;
|
||||||
|
background: #e8f5f1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-btn {
|
||||||
|
color: #5f6f92;
|
||||||
|
background: #eef2f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover,
|
||||||
|
.open-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-empty {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border: 1px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-empty.error {
|
||||||
|
border-color: #eb5757;
|
||||||
|
color: #eb5757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: none;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #2c3e50;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,15 +2,41 @@ import { writable } from "svelte/store";
|
|||||||
import { apiFetch } from "../utils/api.js";
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
export const movieCount = writable(0);
|
export const movieCount = writable(0);
|
||||||
|
let requestSeq = 0;
|
||||||
|
let lastValue = 0;
|
||||||
|
let zeroTimer = null;
|
||||||
|
|
||||||
export async function refreshMovieCount() {
|
export async function refreshMovieCount() {
|
||||||
|
const ticket = ++requestSeq;
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/movies");
|
const resp = await apiFetch("/api/movies");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const list = await resp.json();
|
const list = await resp.json();
|
||||||
movieCount.set(Array.isArray(list) ? list.length : 0);
|
if (ticket !== requestSeq) return;
|
||||||
|
const nextVal = Array.isArray(list) ? list.length : 0;
|
||||||
|
if (nextVal > 0) {
|
||||||
|
if (zeroTimer) {
|
||||||
|
clearTimeout(zeroTimer);
|
||||||
|
zeroTimer = null;
|
||||||
|
}
|
||||||
|
lastValue = nextVal;
|
||||||
|
movieCount.set(nextVal);
|
||||||
|
} else if (lastValue > 0) {
|
||||||
|
if (zeroTimer) clearTimeout(zeroTimer);
|
||||||
|
const zeroTicket = requestSeq;
|
||||||
|
zeroTimer = setTimeout(() => {
|
||||||
|
if (zeroTicket === requestSeq) {
|
||||||
|
lastValue = 0;
|
||||||
|
movieCount.set(0);
|
||||||
|
}
|
||||||
|
zeroTimer = null;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
lastValue = 0;
|
||||||
|
movieCount.set(0);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("⚠️ Movie count güncellenemedi:", err?.message || err);
|
console.warn("⚠️ Movie count güncellenemedi:", err?.message || err);
|
||||||
movieCount.set(0);
|
// Hata durumunda mevcut değeri koru, titreşimi önle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
client/src/stores/musicStore.js
Normal file
42
client/src/stores/musicStore.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
|
export const musicCount = writable(0);
|
||||||
|
let requestSeq = 0;
|
||||||
|
let lastValue = 0;
|
||||||
|
let zeroTimer = null;
|
||||||
|
|
||||||
|
export async function refreshMusicCount() {
|
||||||
|
const ticket = ++requestSeq;
|
||||||
|
try {
|
||||||
|
const resp = await apiFetch("/api/music");
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const list = await resp.json();
|
||||||
|
if (ticket !== requestSeq) return;
|
||||||
|
const nextVal = Array.isArray(list) ? list.length : 0;
|
||||||
|
if (nextVal > 0) {
|
||||||
|
if (zeroTimer) {
|
||||||
|
clearTimeout(zeroTimer);
|
||||||
|
zeroTimer = null;
|
||||||
|
}
|
||||||
|
lastValue = nextVal;
|
||||||
|
musicCount.set(nextVal);
|
||||||
|
} else if (lastValue > 0) {
|
||||||
|
if (zeroTimer) clearTimeout(zeroTimer);
|
||||||
|
const zeroTicket = requestSeq;
|
||||||
|
zeroTimer = setTimeout(() => {
|
||||||
|
if (zeroTicket === requestSeq) {
|
||||||
|
lastValue = 0;
|
||||||
|
musicCount.set(0);
|
||||||
|
}
|
||||||
|
zeroTimer = null;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
lastValue = 0;
|
||||||
|
musicCount.set(0);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("⚠️ Music count güncellenemedi:", err?.message || err);
|
||||||
|
// Hata durumunda mevcut değeri koru, titreşimi önle
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,41 @@ import { writable } from "svelte/store";
|
|||||||
import { apiFetch } from "../utils/api.js";
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
export const tvShowCount = writable(0);
|
export const tvShowCount = writable(0);
|
||||||
|
let requestSeq = 0;
|
||||||
|
let lastValue = 0;
|
||||||
|
let zeroTimer = null;
|
||||||
|
|
||||||
export async function refreshTvShowCount() {
|
export async function refreshTvShowCount() {
|
||||||
|
const ticket = ++requestSeq;
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetch("/api/tvshows");
|
const resp = await apiFetch("/api/tvshows");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const list = await resp.json();
|
const list = await resp.json();
|
||||||
tvShowCount.set(Array.isArray(list) ? list.length : 0);
|
if (ticket !== requestSeq) return;
|
||||||
|
const nextVal = Array.isArray(list) ? list.length : 0;
|
||||||
|
if (nextVal > 0) {
|
||||||
|
if (zeroTimer) {
|
||||||
|
clearTimeout(zeroTimer);
|
||||||
|
zeroTimer = null;
|
||||||
|
}
|
||||||
|
lastValue = nextVal;
|
||||||
|
tvShowCount.set(nextVal);
|
||||||
|
} else if (lastValue > 0) {
|
||||||
|
if (zeroTimer) clearTimeout(zeroTimer);
|
||||||
|
const zeroTicket = requestSeq;
|
||||||
|
zeroTimer = setTimeout(() => {
|
||||||
|
if (zeroTicket === requestSeq) {
|
||||||
|
lastValue = 0;
|
||||||
|
tvShowCount.set(0);
|
||||||
|
}
|
||||||
|
zeroTimer = null;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
lastValue = 0;
|
||||||
|
tvShowCount.set(0);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("⚠️ TV show count güncellenemedi:", err?.message || err);
|
console.warn("⚠️ TV show count güncellenemedi:", err?.message || err);
|
||||||
tvShowCount.set(0);
|
// Hata durumunda mevcut değeri koru, titreşimi önle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
server/server.js
116
server/server.js
@@ -764,9 +764,10 @@ async function finalizeYoutubeJob(job, exitCode) {
|
|||||||
mediaInfo,
|
mediaInfo,
|
||||||
infoJson
|
infoJson
|
||||||
);
|
);
|
||||||
const payload = updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
|
const payloadWithThumb =
|
||||||
const mediaType = payload?.type || "video";
|
updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
|
||||||
const categories = payload?.categories || null;
|
const mediaType = payloadWithThumb?.type || "video";
|
||||||
|
const categories = payloadWithThumb?.categories || null;
|
||||||
upsertInfoFile(job.savePath, {
|
upsertInfoFile(job.savePath, {
|
||||||
infoHash: job.id,
|
infoHash: job.id,
|
||||||
name: job.title,
|
name: job.title,
|
||||||
@@ -5125,8 +5126,13 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
const seriesEpisodeInfo = relWithinRoot
|
const seriesEpisodeInfo = relWithinRoot
|
||||||
? info.seriesEpisodes?.[relWithinRoot] || null
|
? info.seriesEpisodes?.[relWithinRoot] || null
|
||||||
: null;
|
: null;
|
||||||
const mediaCategory =
|
let mediaCategory = fileMeta?.type || null;
|
||||||
fileMeta?.type || (relWithinRoot ? info.type : null) || null;
|
if (!mediaCategory) {
|
||||||
|
const canInheritFromInfo = !relWithinRoot || isVideo;
|
||||||
|
if (canInheritFromInfo && info.type) {
|
||||||
|
mediaCategory = info.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
const isPrimaryVideo =
|
const isPrimaryVideo =
|
||||||
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
|
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
|
||||||
const displayName = entry.name;
|
const displayName = entry.name;
|
||||||
@@ -6078,6 +6084,89 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function collectMusicEntries() {
|
||||||
|
const entries = [];
|
||||||
|
const dirEntries = fs
|
||||||
|
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
|
||||||
|
.filter((dirent) => dirent.isDirectory());
|
||||||
|
|
||||||
|
for (const dirent of dirEntries) {
|
||||||
|
const folder = sanitizeRelative(dirent.name);
|
||||||
|
if (!folder) continue;
|
||||||
|
// Klasörün tamamı çöpe taşınmışsa atla
|
||||||
|
if (isPathTrashed(folder, "", true)) continue;
|
||||||
|
const info = readInfoForRoot(folder) || {};
|
||||||
|
const files = info.files || {};
|
||||||
|
const fileKeys = Object.keys(files);
|
||||||
|
if (!fileKeys.length) continue;
|
||||||
|
|
||||||
|
let targetPath = info.primaryVideoPath;
|
||||||
|
if (targetPath && files[targetPath]?.type !== "music") {
|
||||||
|
targetPath = null;
|
||||||
|
}
|
||||||
|
if (!targetPath) {
|
||||||
|
targetPath =
|
||||||
|
fileKeys.find((key) => 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;
|
||||||
|
if (mediaType !== "music") continue;
|
||||||
|
|
||||||
|
const metadataPath = path.join(YT_DATA_ROOT, folder, "metadata.json");
|
||||||
|
let metadata = null;
|
||||||
|
if (fs.existsSync(metadataPath)) {
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ YT metadata okunamadı (${metadataPath}): ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysArray = fileKeys;
|
||||||
|
const fileIndex = Math.max(keysArray.indexOf(targetPath), 0);
|
||||||
|
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);
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
id: `${folder}:${targetPath}`,
|
||||||
|
folder,
|
||||||
|
infoHash,
|
||||||
|
fileIndex,
|
||||||
|
filePath: targetPath,
|
||||||
|
title,
|
||||||
|
added: info.added || info.createdAt || null,
|
||||||
|
size: fileMeta?.size || 0,
|
||||||
|
url:
|
||||||
|
metadata?.url ||
|
||||||
|
fileMeta?.youtube?.url ||
|
||||||
|
fileMeta?.youtube?.videoId
|
||||||
|
? `https://www.youtube.com/watch?v=${fileMeta.youtube.videoId}`
|
||||||
|
: null,
|
||||||
|
thumbnail,
|
||||||
|
categories: metadata?.categories || fileMeta?.categories || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.sort((a, b) => (b.added || 0) - (a.added || 0));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/music", requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
const entries = collectMusicEntries();
|
||||||
|
res.json(entries);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("🎵 Music API error:", err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function rebuildTvMetadata({ clearCache = false } = {}) {
|
async function rebuildTvMetadata({ clearCache = false } = {}) {
|
||||||
if (!TVDB_API_KEY) {
|
if (!TVDB_API_KEY) {
|
||||||
throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil.");
|
throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil.");
|
||||||
@@ -6330,6 +6419,23 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
|
|||||||
return streamLocalFile(absPath, range, res);
|
return streamLocalFile(absPath, range, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const info = readInfoForRoot(req.params.hash);
|
||||||
|
if (info && info.files) {
|
||||||
|
const fileKeys = Object.keys(info.files);
|
||||||
|
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)) {
|
||||||
|
return streamLocalFile(absPath, range, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(404).end();
|
return res.status(404).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user