TMDB Entegrasyonu
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Router, Route } from "svelte-routing";
|
import { Router, Route } from "svelte-routing";
|
||||||
|
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 Files from "./routes/Files.svelte";
|
import Files from "./routes/Files.svelte";
|
||||||
import Transfers from "./routes/Transfers.svelte";
|
import Transfers from "./routes/Transfers.svelte";
|
||||||
import Sharing from "./routes/Sharing.svelte";
|
import Sharing from "./routes/Sharing.svelte";
|
||||||
import Trash from "./routes/Trash.svelte";
|
import Trash from "./routes/Trash.svelte";
|
||||||
|
import Movies from "./routes/Movies.svelte";
|
||||||
import Login from "./routes/Login.svelte";
|
import Login from "./routes/Login.svelte";
|
||||||
|
import { refreshMovieCount } from "./stores/movieStore.js";
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
@@ -21,6 +24,12 @@
|
|||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (token) {
|
||||||
|
refreshMovieCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if token}
|
{#if token}
|
||||||
@@ -35,6 +44,7 @@
|
|||||||
|
|
||||||
<Route path="/" component={Files} />
|
<Route path="/" component={Files} />
|
||||||
<Route path="/files" component={Files} />
|
<Route path="/files" component={Files} />
|
||||||
|
<Route path="/movies" component={Movies} />
|
||||||
<Route path="/transfers" component={Transfers} />
|
<Route path="/transfers" component={Transfers} />
|
||||||
<Route path="/sharing" component={Sharing} />
|
<Route path="/sharing" component={Sharing} />
|
||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Link } from "svelte-routing";
|
import { Link } from "svelte-routing";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
|
import { movieCount } from "../stores/movieStore.js";
|
||||||
|
|
||||||
export let menuOpen = false;
|
export let menuOpen = false;
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
let hasMovies = false;
|
||||||
|
|
||||||
|
const unsubscribe = movieCount.subscribe((count) => {
|
||||||
|
hasMovies = (count ?? 0) > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
// Menü öğesine tıklanınca sidebar'ı kapat
|
// Menü öğesine tıklanınca sidebar'ı kapat
|
||||||
function handleLinkClick() {
|
function handleLinkClick() {
|
||||||
@@ -27,6 +37,20 @@
|
|||||||
Files
|
Files
|
||||||
</Link>
|
</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
|
<Link
|
||||||
to="/transfers"
|
to="/transfers"
|
||||||
class="item"
|
class="item"
|
||||||
|
|||||||
@@ -2,18 +2,42 @@
|
|||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { API, apiFetch } from "../utils/api.js";
|
import { API, apiFetch } from "../utils/api.js";
|
||||||
import { cleanFileName } from "../utils/filename.js";
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
import { refreshMovieCount } from "../stores/movieStore.js";
|
||||||
let files = [];
|
let files = [];
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let selectedVideo = null;
|
let selectedVideo = null;
|
||||||
let subtitleURL = null;
|
let subtitleURL = null;
|
||||||
let subtitleLang = "en";
|
let subtitleLang = "en";
|
||||||
let subtitleLabel = "Custom Subtitles";
|
let subtitleLabel = "Custom Subtitles";
|
||||||
let viewMode = "grid";
|
const VIEW_KEY = "filesViewMode";
|
||||||
let selectedItems = new Set();
|
let viewMode = "grid";
|
||||||
let allSelected = false;
|
if (typeof window !== "undefined") {
|
||||||
// 🎬 Player kontrolleri
|
const storedView = window.localStorage.getItem(VIEW_KEY);
|
||||||
let videoEl;
|
if (storedView === "grid" || storedView === "list") {
|
||||||
let isPlaying = false;
|
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 currentTime = 0;
|
||||||
let duration = 0;
|
let duration = 0;
|
||||||
let volume = 1;
|
let volume = 1;
|
||||||
@@ -39,6 +63,8 @@
|
|||||||
[...selectedItems].filter((name) => existing.has(name)),
|
[...selectedItems].filter((name) => existing.has(name)),
|
||||||
);
|
);
|
||||||
allSelected = files.length > 0 && selectedItems.size === files.length;
|
allSelected = files.length > 0 && selectedItems.size === files.length;
|
||||||
|
tryAutoPlay();
|
||||||
|
refreshMovieCount();
|
||||||
}
|
}
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (!bytes) return "0 MB";
|
if (!bytes) return "0 MB";
|
||||||
@@ -52,8 +78,33 @@
|
|||||||
if (Number.isNaN(date.getTime())) return "—";
|
if (Number.isNaN(date.getTime())) return "—";
|
||||||
return date.toLocaleString();
|
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() {
|
function toggleView() {
|
||||||
viewMode = viewMode === "grid" ? "list" : "grid";
|
viewMode = viewMode === "grid" ? "list" : "grid";
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.localStorage.setItem(VIEW_KEY, viewMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function toggleSelection(file) {
|
function toggleSelection(file) {
|
||||||
const next = new Set(selectedItems);
|
const next = new Set(selectedItems);
|
||||||
@@ -80,6 +131,27 @@
|
|||||||
selectedItems = new Set();
|
selectedItems = new Set();
|
||||||
allSelected = false;
|
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) {
|
async function openModal(f) {
|
||||||
stopCurrentVideo();
|
stopCurrentVideo();
|
||||||
videoEl = null;
|
videoEl = null;
|
||||||
@@ -251,6 +323,7 @@
|
|||||||
|
|
||||||
selectedItems = new Set(failed);
|
selectedItems = new Set(failed);
|
||||||
allSelected = failed.length > 0 && failed.length === files.length;
|
allSelected = failed.length > 0 && failed.length === files.length;
|
||||||
|
await refreshMovieCount();
|
||||||
}
|
}
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadFiles(); // önce dosyaları getir
|
await loadFiles(); // önce dosyaları getir
|
||||||
@@ -288,6 +361,32 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
function handleKey(e) {
|
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;
|
const isCmd = e.metaKey || e.ctrlKey;
|
||||||
if (isCmd && e.key.toLowerCase() === "a") {
|
if (isCmd && e.key.toLowerCase() === "a") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -312,8 +411,13 @@
|
|||||||
|
|
||||||
<section class="files" on:click={handleFilesClick}>
|
<section class="files" on:click={handleFilesClick}>
|
||||||
<div class="files-header">
|
<div class="files-header">
|
||||||
|
<div class="header-title">
|
||||||
<h2>Media Library</h2>
|
<h2>Media Library</h2>
|
||||||
|
</div>
|
||||||
<div class="header-actions">
|
<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}
|
{#if files.length > 0 && selectedItems.size > 0}
|
||||||
<button
|
<button
|
||||||
class="select-all-btn"
|
class="select-all-btn"
|
||||||
@@ -394,6 +498,35 @@
|
|||||||
{f.tracker ? f.tracker : "Bilinmiyor"}
|
{f.tracker ? f.tracker : "Bilinmiyor"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="media-type-icon">
|
<div class="media-type-icon">
|
||||||
@@ -733,6 +866,16 @@
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.selection-count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6a6a6a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -967,6 +1110,32 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
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 {
|
.status-badge {
|
||||||
color: #2f8a4d;
|
color: #2f8a4d;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
1047
client/src/routes/Movies.svelte
Normal file
1047
client/src/routes/Movies.svelte
Normal file
File diff suppressed because it is too large
Load Diff
16
client/src/stores/movieStore.js
Normal file
16
client/src/stores/movieStore.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
* Dosya adını temizleyip başlık/yıl bilgisi çıkarır.
|
||||||
* Örnek:
|
* Örn: The.Astronaut.2025.1080p.WEBRip → "The Astronaut (2025)"
|
||||||
* 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"
|
|
||||||
*/
|
*/
|
||||||
export function cleanFileName(fullPath) {
|
export function cleanFileName(fullPath) {
|
||||||
if (!fullPath) return "";
|
if (!fullPath) return "";
|
||||||
|
|
||||||
// 1️⃣ Klasör yolunu kaldır
|
const baseName = fullPath.split("/").pop() || "";
|
||||||
let name = fullPath.split("/").pop();
|
const withoutExt = baseName.replace(/\.[^.]+$/, "");
|
||||||
|
|
||||||
// 2️⃣ Uzantıyı kaldır
|
const episodeMatch = withoutExt.match(/(S\d{1,2}E\d{1,2})/i);
|
||||||
name = name.replace(/\.[^.]+$/, "");
|
const { title, year } = extractTitleAndYear(withoutExt);
|
||||||
|
|
||||||
// 3️⃣ Noktaları ve alt tireleri boşluğa çevir
|
let result = titleCase(title);
|
||||||
name = name.replace(/[._]+/g, " ");
|
|
||||||
|
|
||||||
// 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) {
|
if (episodeMatch) {
|
||||||
const epTag = episodeMatch[0].toUpperCase();
|
const tag = episodeMatch[0].toUpperCase();
|
||||||
// örnek: Gen V S02E08 Cavallo di Troia
|
result = result
|
||||||
name = name.replace(episodeMatch[0], epTag);
|
? `${result} ${tag}`
|
||||||
|
: tag;
|
||||||
|
} else if (year) {
|
||||||
|
result = result ? `${result} (${year})` : `${withoutExt} (${year})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8️⃣ Baş harfleri büyüt (küçük kelimeleri koruyarak)
|
if (!result) {
|
||||||
name = name
|
result = withoutExt.replace(/[._]+/g, " ");
|
||||||
.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();
|
|
||||||
|
|
||||||
return name;
|
return result.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
USERNAME: ${USERNAME}
|
USERNAME: ${USERNAME}
|
||||||
PASSWORD: ${PASSWORD}
|
PASSWORD: ${PASSWORD}
|
||||||
|
TMDB_API_KEY: ${TMDB_API_KEY}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"webtorrent": "^1.9.7",
|
"webtorrent": "^1.9.7",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
|
|||||||
870
server/server.js
870
server/server.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user