TMDB Entegrasyonu

This commit is contained in:
2025-10-27 06:05:34 +03:00
parent 1760441ce7
commit 66ac562bcd
9 changed files with 2292 additions and 136 deletions

View File

@@ -1,12 +1,15 @@
<script>
import { Router, Route } from "svelte-routing";
import { onMount } from "svelte";
import Sidebar from "./components/Sidebar.svelte";
import Topbar from "./components/Topbar.svelte";
import Files from "./routes/Files.svelte";
import Transfers from "./routes/Transfers.svelte";
import Sharing from "./routes/Sharing.svelte";
import Trash from "./routes/Trash.svelte";
import Movies from "./routes/Movies.svelte";
import Login from "./routes/Login.svelte";
import { refreshMovieCount } from "./stores/movieStore.js";
const token = localStorage.getItem("token");
@@ -21,6 +24,12 @@
function closeSidebar() {
menuOpen = false;
}
onMount(() => {
if (token) {
refreshMovieCount();
}
});
</script>
{#if token}
@@ -35,6 +44,7 @@
<Route path="/" component={Files} />
<Route path="/files" component={Files} />
<Route path="/movies" component={Movies} />
<Route path="/transfers" component={Transfers} />
<Route path="/sharing" component={Sharing} />
<Route path="/trash" component={Trash} />

View File

@@ -1,9 +1,19 @@
<script>
import { Link } from "svelte-routing";
import { createEventDispatcher } from "svelte";
import { createEventDispatcher, onDestroy } from "svelte";
import { movieCount } from "../stores/movieStore.js";
export let menuOpen = false;
const dispatch = createEventDispatcher();
let hasMovies = false;
const unsubscribe = movieCount.subscribe((count) => {
hasMovies = (count ?? 0) > 0;
});
onDestroy(() => {
unsubscribe();
});
// Menü öğesine tıklanınca sidebar'ı kapat
function handleLinkClick() {
@@ -27,6 +37,20 @@
Files
</Link>
{#if hasMovies}
<Link
to="/movies"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<i class="fa-solid fa-film icon"></i>
Movies
</Link>
{/if}
<Link
to="/transfers"
class="item"

View File

@@ -2,18 +2,42 @@
import { onMount, tick } from "svelte";
import { API, apiFetch } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js";
let files = [];
let showModal = false;
let selectedVideo = null;
let subtitleURL = null;
let subtitleLang = "en";
let subtitleLabel = "Custom Subtitles";
let viewMode = "grid";
let selectedItems = new Set();
let allSelected = false;
// 🎬 Player kontrolleri
let videoEl;
let isPlaying = false;
const VIEW_KEY = "filesViewMode";
let viewMode = "grid";
if (typeof window !== "undefined") {
const storedView = window.localStorage.getItem(VIEW_KEY);
if (storedView === "grid" || storedView === "list") {
viewMode = storedView;
}
}
let selectedItems = new Set();
let allSelected = false;
let pendingPlayTarget = null;
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
const playParam = params.get("play");
if (playParam) {
try {
pendingPlayTarget = decodeURIComponent(playParam);
} catch (err) {
pendingPlayTarget = playParam;
}
params.delete("play");
const search = params.toString();
const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
window.history.replaceState({}, "", newUrl);
}
}
// 🎬 Player kontrolleri
let videoEl;
let isPlaying = false;
let currentTime = 0;
let duration = 0;
let volume = 1;
@@ -39,6 +63,8 @@
[...selectedItems].filter((name) => existing.has(name)),
);
allSelected = files.length > 0 && selectedItems.size === files.length;
tryAutoPlay();
refreshMovieCount();
}
function formatSize(bytes) {
if (!bytes) return "0 MB";
@@ -52,8 +78,33 @@
if (Number.isNaN(date.getTime())) return "—";
return date.toLocaleString();
}
function formatVideoCodec(info) {
if (!info) return null;
const codec = info.codec ? info.codec.toUpperCase() : null;
const resolution =
info.resolution || (info.height ? `${info.height}p` : null);
return [codec, resolution].filter(Boolean).join(" · ");
}
function formatAudioCodec(info) {
if (!info) return null;
const codec = info.codec ? info.codec.toUpperCase() : null;
let channels = null;
if (info.channelLayout) channels = info.channelLayout.toUpperCase();
else if (info.channels) {
channels =
info.channels === 6
? "5.1"
: info.channels === 2
? "2.0"
: `${info.channels}`;
}
return [codec, channels].filter(Boolean).join(" · ");
}
function toggleView() {
viewMode = viewMode === "grid" ? "list" : "grid";
if (typeof window !== "undefined") {
window.localStorage.setItem(VIEW_KEY, viewMode);
}
}
function toggleSelection(file) {
const next = new Set(selectedItems);
@@ -80,6 +131,27 @@
selectedItems = new Set();
allSelected = false;
}
function tryAutoPlay() {
if (!pendingPlayTarget || files.length === 0) return;
const normalizedTarget = pendingPlayTarget
.replace(/^\.?\//, "")
.replace(/\\/g, "/");
const candidate =
files.find((f) => {
const normalizedName = f.name
.replace(/^\.?\//, "")
.replace(/\\/g, "/");
return (
normalizedName === normalizedTarget ||
normalizedName.endsWith(normalizedTarget)
);
}) || null;
if (candidate) {
pendingPlayTarget = null;
openModal(candidate);
}
}
async function openModal(f) {
stopCurrentVideo();
videoEl = null;
@@ -251,6 +323,7 @@
selectedItems = new Set(failed);
allSelected = failed.length > 0 && failed.length === files.length;
await refreshMovieCount();
}
onMount(async () => {
await loadFiles(); // önce dosyaları getir
@@ -288,6 +361,32 @@
}
};
function handleKey(e) {
const active = document.activeElement;
const tag = active?.tagName;
const type = active?.type?.toLowerCase();
const isTextInput =
tag === "INPUT" &&
[
"text",
"search",
"email",
"password",
"number",
"url",
"tel"
].includes(type);
const isEditable =
(tag === "TEXTAREA" || isTextInput || active?.isContentEditable) ?? false;
if (e.metaKey && e.key && e.key.toLowerCase() === "backspace") {
if (isEditable) return;
if (selectedItems.size > 0) {
e.preventDefault();
deleteSelectedFiles();
}
return;
}
const isCmd = e.metaKey || e.ctrlKey;
if (isCmd && e.key.toLowerCase() === "a") {
e.preventDefault();
@@ -312,8 +411,13 @@
<section class="files" on:click={handleFilesClick}>
<div class="files-header">
<h2>Media Library</h2>
<div class="header-title">
<h2>Media Library</h2>
</div>
<div class="header-actions">
{#if files.length > 0 && selectedItems.size > 0}
<span class="selection-count">{selectedItems.size} dosya seçildi</span>
{/if}
{#if files.length > 0 && selectedItems.size > 0}
<button
class="select-all-btn"
@@ -394,6 +498,35 @@
{f.tracker ? f.tracker : "Bilinmiyor"}
</span>
</div>
{#if f.mediaInfo?.video || f.mediaInfo?.audio}
<div class="meta-line codecs">
{#if f.extension}
<span class="codec-chip file-type">
{#if f.type?.startsWith("image/")}
<i class="fa-solid fa-file-image"></i>
{:else}
<i class="fa-solid fa-file-video"></i>
{/if}
{f.extension.toUpperCase()}
</span>
{/if}
{#if f.mediaInfo?.video}
<span class="codec-chip">
<i class="fa-solid fa-film"></i>
{formatVideoCodec(f.mediaInfo.video)}
</span>
{/if}
{#if f.mediaInfo?.video && f.mediaInfo?.audio}
<span class="codec-separator">|</span>
{/if}
{#if f.mediaInfo?.audio}
<span class="codec-chip">
<i class="fa-solid fa-volume-high"></i>
{formatAudioCodec(f.mediaInfo.audio)}
</span>
{/if}
</div>
{/if}
</div>
</div>
<div class="media-type-icon">
@@ -733,6 +866,16 @@
margin-bottom: 18px;
gap: 12px;
}
.header-title {
display: flex;
align-items: flex-end;
gap: 6px;
}
.selection-count {
font-size: 13px;
color: #6a6a6a;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
@@ -967,6 +1110,32 @@
display: inline-block;
vertical-align: middle;
}
.meta-line.codecs {
display: flex;
align-items: center;
gap: 12px;
color: #5a5a5a;
font-size: 13px;
}
.codec-chip {
display: inline-flex;
align-items: center;
gap: 6px;
color: #4e4e4e;
font-weight: 500;
}
.codec-chip.file-type {
color: #1f1f1f;
text-transform: uppercase;
}
.codec-chip i {
color: #ffc107;
font-size: 12px;
}
.codec-separator {
color: #7a7a7a;
font-weight: 500;
}
.status-badge {
color: #2f8a4d;
font-weight: 600;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
import { writable } from "svelte/store";
import { apiFetch } from "../utils/api.js";
export const movieCount = writable(0);
export async function refreshMovieCount() {
try {
const resp = await apiFetch("/api/movies");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const list = await resp.json();
movieCount.set(Array.isArray(list) ? list.length : 0);
} catch (err) {
console.warn("⚠️ Movie count güncellenemedi:", err?.message || err);
movieCount.set(0);
}
}

View File

@@ -1,98 +1,176 @@
const LOWERCASE_WORDS = new Set(["di", "da", "de", "of", "and", "the", "la"]);
const IGNORED_TOKENS = new Set(
[
"hdrip",
"hdr",
"webrip",
"webdl",
"dl",
"web-dl",
"blu",
"bluray",
"bdrip",
"dvdrip",
"remux",
"multi",
"audio",
"aac",
"ac3",
"ddp",
"dts",
"xvid",
"x264",
"x265",
"x266",
"h264",
"h265",
"hevc",
"hdr10",
"hdr10plus",
"amzn",
"nf",
"netflix",
"disney",
"imax",
"atmos",
"dubbed",
"dublado",
"ita",
"eng",
"turkce",
"multi-audio",
"eazy",
"tbmovies",
"tbm",
"bone"
].map((t) => t.toLowerCase())
);
const QUALITY_PATTERNS = [
/^\d{3,4}p$/,
/^s\d{1,2}e\d{1,2}$/i,
/^(ac3|aac\d*|ddp\d*|dts|dubbed|dual|multi|hc)$/i,
/^(x|h)?26[45]$/i
];
export function extractTitleAndYear(rawName) {
if (!rawName) return { title: "", year: null };
const withoutExt = rawName.replace(/\.[^/.]+$/, "");
const normalized = withoutExt
.replace(/[\[\]\(\)\-]/g, " ")
.replace(/[._]/g, " ");
const tokens = normalized
.split(/\s+/)
.map((token) => token.trim())
.filter(Boolean);
if (!tokens.length) {
return { title: withoutExt.trim(), year: null };
}
const yearIndex = tokens.findIndex((token) => /^(19|20)\d{2}$/.test(token));
if (yearIndex > 0) {
const yearToken = tokens[yearIndex];
const candidateTokens = tokens.slice(0, yearIndex);
const filteredTitleTokens = candidateTokens.filter((token) => {
const lower = token.toLowerCase();
if (IGNORED_TOKENS.has(lower)) return false;
if (/^\d{3,4}p$/.test(lower)) return false;
if (/^(ac3|aac\d*|ddp\d*|dts|dubbed|dual|multi|hc)$/i.test(lower))
return false;
if (/^(x|h)?26[45]$/i.test(lower)) return false;
if (lower.includes("hdrip") || lower.includes("web-dl")) return false;
if (lower.includes("multi-audio")) return false;
return true;
});
const titleTokens = filteredTitleTokens.length
? filteredTitleTokens
: candidateTokens;
const title = titleTokens.join(" ").replace(/\s+/g, " ").trim();
return {
title: title || withoutExt.trim(),
year: Number(yearToken)
};
}
let year = null;
const filtered = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const lower = token.toLowerCase();
if (!year && /^(19|20)\d{2}$/.test(lower)) {
year = Number(lower);
continue;
}
if (QUALITY_PATTERNS.some((pattern) => pattern.test(token))) continue;
if (lower === "web" && tokens[i + 1]?.toLowerCase() === "dl") {
i += 1;
continue;
}
if (IGNORED_TOKENS.has(lower)) continue;
if (lower.includes("multi-audio")) continue;
filtered.push(token);
}
const title = filtered.join(" ").replace(/\s+/g, " ").trim();
return {
title: title || withoutExt.trim(),
year
};
}
function titleCase(value) {
if (!value) return "";
return value
.split(" ")
.filter(Boolean)
.map((segment, index) => {
const lower = segment.toLowerCase();
if (index !== 0 && LOWERCASE_WORDS.has(lower)) {
return lower;
}
return lower.charAt(0).toUpperCase() + lower.slice(1);
})
.join(" ");
}
/**
* Dosya adını temizler ve sadeleştirir.
* Örnek:
* The.Astronaut.2025.1080p.WEBRip.x265-KONTRAST
* → "The Astronaut (2025)"
* 1761244874124/Gen.V.S02E08.Cavallo.di.Troia.ITA.ENG.1080p.AMZN.WEB-DL.DDP5.1.H.264-MeM.GP.mkv
* → "Gen V S02E08 Cavallo Di Troia"
* Dosya adını temizleyip başlık/yıl bilgisi çıkarır.
* Örn: The.Astronaut.2025.1080p.WEBRip → "The Astronaut (2025)"
*/
export function cleanFileName(fullPath) {
if (!fullPath) return "";
// 1⃣ Klasör yolunu kaldır
let name = fullPath.split("/").pop();
const baseName = fullPath.split("/").pop() || "";
const withoutExt = baseName.replace(/\.[^.]+$/, "");
// 2⃣ Uzantıyı kaldır
name = name.replace(/\.[^.]+$/, "");
const episodeMatch = withoutExt.match(/(S\d{1,2}E\d{1,2})/i);
const { title, year } = extractTitleAndYear(withoutExt);
// 3⃣ Noktaları ve alt tireleri boşluğa çevir
name = name.replace(/[._]+/g, " ");
let result = titleCase(title);
// 4⃣ Gereksiz etiketleri kaldır
const trashWords = [
"1080p",
"720p",
"2160p",
"4k",
"bluray",
"web[- ]?dl",
"webrip",
"hdrip",
"x264",
"x265",
"hevc",
"aac",
"h264",
"h265",
"ddp5",
"dvdrip",
"brrip",
"remux",
"multi",
"sub",
"subs",
"turkce",
"ita",
"eng",
"dublado",
"dubbed",
"extended",
"unrated",
"repack",
"proper",
"kontrast",
"yify",
"ettv",
"rarbg",
"hdtv",
"amzn",
"nf",
"netflix",
"mem",
"gp"
];
const trashRegex = new RegExp(`\\b(${trashWords.join("|")})\\b`, "gi");
name = name.replace(trashRegex, " ");
// 5⃣ Parantez veya köşeli parantez içindekileri kaldır
name = name.replace(/[\[\(].*?[\]\)]/g, " ");
// 6⃣ Fazla tireleri ve sayıları temizle
name = name
.replace(/[-]+/g, " ")
.replace(/\b\d{3,4}\b/g, " ") // tek başına 1080, 2025 gibi
.replace(/\s{2,}/g, " ")
.trim();
// 7⃣ Dizi formatını (S02E08) koru
const episodeMatch = name.match(/(S\d{1,2}E\d{1,2})/i);
if (episodeMatch) {
const epTag = episodeMatch[0].toUpperCase();
// örnek: Gen V S02E08 Cavallo di Troia
name = name.replace(episodeMatch[0], epTag);
const tag = episodeMatch[0].toUpperCase();
result = result
? `${result} ${tag}`
: tag;
} else if (year) {
result = result ? `${result} (${year})` : `${withoutExt} (${year})`;
}
// 8⃣ Baş harfleri büyüt (küçük kelimeleri koruyarak)
name = name
.split(" ")
.filter((w) => w.length > 0)
.map((w) => {
if (["di", "da", "de", "of", "and", "the"].includes(w.toLowerCase()))
return w.toLowerCase();
return w[0].toUpperCase() + w.slice(1).toLowerCase();
})
.join(" ")
.trim();
if (!result) {
result = withoutExt.replace(/[._]+/g, " ");
}
return name;
return result.trim();
}

View File

@@ -14,3 +14,4 @@ services:
environment:
USERNAME: ${USERNAME}
PASSWORD: ${PASSWORD}
TMDB_API_KEY: ${TMDB_API_KEY}

View File

@@ -10,7 +10,8 @@
"express": "^4.19.2",
"mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1",
"node-fetch": "^3.3.2",
"webtorrent": "^1.9.7",
"ws": "^8.18.0"
}
}
}

File diff suppressed because it is too large Load Diff