Dizi ve filmleri eşleştirme modu eklendi.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -304,13 +304,13 @@ async function loadMovies() {
|
|||||||
function posterUrl(movie) {
|
function posterUrl(movie) {
|
||||||
if (!movie.poster) return null;
|
if (!movie.poster) return null;
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return `${API}${movie.poster}?token=${token}`;
|
return `${API}${movie.poster}?token=${token}&t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function backdropUrl(movie) {
|
function backdropUrl(movie) {
|
||||||
if (!movie.backdrop) return null;
|
if (!movie.backdrop) return null;
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return `${API}${movie.backdrop}?token=${token}`;
|
return `${API}${movie.backdrop}?token=${token}&t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMovies() {
|
async function refreshMovies() {
|
||||||
@@ -339,6 +339,16 @@ async function loadMovies() {
|
|||||||
const handleKey = (event) => {
|
const handleKey = (event) => {
|
||||||
if (!showPlayerModal) return;
|
if (!showPlayerModal) return;
|
||||||
if (isEditableTarget(event.target)) return;
|
if (isEditableTarget(event.target)) return;
|
||||||
|
|
||||||
|
const isCmd = event.metaKey || event.ctrlKey;
|
||||||
|
if (isCmd && event.key.toLowerCase() === "a") {
|
||||||
|
// Text input'larda çalıştırma
|
||||||
|
if (isEditableTarget(event.target)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
// Movies sayfasında tümünü seçme işlevi yok, sadece engelleme yapıyoruz
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closePlayer();
|
closePlayer();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ let canPlayNext = false;
|
|||||||
function assetUrl(pathname) {
|
function assetUrl(pathname) {
|
||||||
if (!pathname) return null;
|
if (!pathname) return null;
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return `${API}${pathname}?token=${token}`;
|
return `${API}${pathname}?token=${token}&t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function posterUrl(show) {
|
function posterUrl(show) {
|
||||||
@@ -250,7 +250,7 @@ let canPlayNext = false;
|
|||||||
|
|
||||||
function mapEpisodeToPlayerItem(show, episode) {
|
function mapEpisodeToPlayerItem(show, episode) {
|
||||||
if (!show || !episode?.videoPath) return null;
|
if (!show || !episode?.videoPath) return null;
|
||||||
const name = episode.videoPath;
|
const name = (episode.videoPath || "").replace(/^\/+/, "");
|
||||||
const ext = name.split(".").pop()?.toLowerCase() || "";
|
const ext = name.split(".").pop()?.toLowerCase() || "";
|
||||||
const inferredType = ext ? `video/${ext}` : "video/mp4";
|
const inferredType = ext ? `video/${ext}` : "video/mp4";
|
||||||
const size =
|
const size =
|
||||||
@@ -276,6 +276,7 @@ let canPlayNext = false;
|
|||||||
|
|
||||||
$: selectedName = selectedVideo?.name ?? "";
|
$: selectedName = selectedVideo?.name ?? "";
|
||||||
$: encName = selectedName ? encodeURIComponent(selectedName) : "";
|
$: encName = selectedName ? encodeURIComponent(selectedName) : "";
|
||||||
|
$: downloadHref = encName ? `${API}/downloads/${encName}` : "#";
|
||||||
$: selectedLabel = selectedVideo?.episode
|
$: selectedLabel = selectedVideo?.episode
|
||||||
? `${selectedVideo.show.title} · ${formatEpisodeCode(
|
? `${selectedVideo.show.title} · ${formatEpisodeCode(
|
||||||
selectedVideo.episode
|
selectedVideo.episode
|
||||||
@@ -346,6 +347,18 @@ async function openVideoAtIndex(index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video element'in yüklendiğinden emin ol
|
||||||
|
await tick();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (videoEl) {
|
||||||
|
console.log("Video element found after timeout:", videoEl);
|
||||||
|
console.log("Video src:", videoEl.src);
|
||||||
|
console.log("Video readyState:", videoEl.readyState);
|
||||||
|
} else {
|
||||||
|
console.error("Video element not found after timeout");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopCurrentVideo() {
|
function stopCurrentVideo() {
|
||||||
@@ -375,7 +388,8 @@ async function openVideoAtIndex(index) {
|
|||||||
function getVideoURL() {
|
function getVideoURL() {
|
||||||
if (!selectedName) return "";
|
if (!selectedName) return "";
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return `${API}/media/${encName}?token=${token}`;
|
// selectedName zaten encode edilmiş, tekrar encode etme
|
||||||
|
return `${API}/media/${selectedName}?token=${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function playEpisodeFromCard(episode) {
|
function playEpisodeFromCard(episode) {
|
||||||
@@ -532,6 +546,16 @@ async function openVideoAtIndex(index) {
|
|||||||
const handleKey = (event) => {
|
const handleKey = (event) => {
|
||||||
if (!showPlayerModal) return;
|
if (!showPlayerModal) return;
|
||||||
if (isEditableTarget(event.target)) return;
|
if (isEditableTarget(event.target)) return;
|
||||||
|
|
||||||
|
const isCmd = event.metaKey || event.ctrlKey;
|
||||||
|
if (isCmd && event.key.toLowerCase() === "a") {
|
||||||
|
// Text input'larda çalıştırma
|
||||||
|
if (isEditableTarget(event.target)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
// TvShows sayfasında tümünü seçme işlevi yok, sadece engelleme yapıyoruz
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closePlayer();
|
closePlayer();
|
||||||
@@ -787,8 +811,13 @@ async function openVideoAtIndex(index) {
|
|||||||
src={getVideoURL()}
|
src={getVideoURL()}
|
||||||
class="video-element"
|
class="video-element"
|
||||||
playsinline
|
playsinline
|
||||||
|
controls={false}
|
||||||
|
preload="metadata"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
type={selectedVideo?.type || "video/mp4"}
|
||||||
on:timeupdate={updateProgress}
|
on:timeupdate={updateProgress}
|
||||||
on:loadedmetadata={async () => {
|
on:loadedmetadata={async () => {
|
||||||
|
console.log("Video metadata loaded");
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
updateDuration();
|
updateDuration();
|
||||||
@@ -805,7 +834,37 @@ async function openVideoAtIndex(index) {
|
|||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
on:loadeddata={() => {
|
||||||
|
console.log("Video data loaded");
|
||||||
|
}}
|
||||||
|
on:canplay={() => {
|
||||||
|
console.log("Video can play");
|
||||||
|
}}
|
||||||
|
on:error={(e) => {
|
||||||
|
console.error("Video error:", e);
|
||||||
|
}}
|
||||||
on:ended={() => (isPlaying = false)}
|
on:ended={() => (isPlaying = false)}
|
||||||
|
on:loadstart={() => {
|
||||||
|
console.log("Video load start");
|
||||||
|
}}
|
||||||
|
on:canplaythrough={() => {
|
||||||
|
console.log("Video can play through");
|
||||||
|
}}
|
||||||
|
on:stalled={() => {
|
||||||
|
console.log("Video stalled");
|
||||||
|
}}
|
||||||
|
on:suspend={() => {
|
||||||
|
console.log("Video suspended");
|
||||||
|
}}
|
||||||
|
on:abort={() => {
|
||||||
|
console.log("Video aborted");
|
||||||
|
}}
|
||||||
|
on:emptied={() => {
|
||||||
|
console.log("Video emptied");
|
||||||
|
}}
|
||||||
|
on:waiting={() => {
|
||||||
|
console.log("Video waiting");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if subtitleURL}
|
{#if subtitleURL}
|
||||||
<track
|
<track
|
||||||
@@ -860,7 +919,7 @@ async function openVideoAtIndex(index) {
|
|||||||
<i class="fa-solid fa-expand"></i>
|
<i class="fa-solid fa-expand"></i>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={selectedName ? `${API}/downloads/${selectedName}` : "#"}
|
href={downloadHref}
|
||||||
download={selectedName || undefined}
|
download={selectedName || undefined}
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
title="Download"
|
title="Download"
|
||||||
@@ -1235,15 +1294,26 @@ async function openVideoAtIndex(index) {
|
|||||||
background: rgba(0, 0, 0, 0.25);
|
background: rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-list::-webkit-scrollbar {
|
.episode-list::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-list::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-list::-webkit-scrollbar-thumb {
|
.episode-list::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.18);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
border-radius: 999px;
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-card {
|
.episode-card {
|
||||||
@@ -1345,98 +1415,117 @@ async function openVideoAtIndex(index) {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🎞️ Film oynatıcı ile aynı düzen */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.75);
|
backdrop-filter: blur(4px);
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 5000;
|
z-index: 6000;
|
||||||
padding: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
width: min(960px, 95vw);
|
width: 70%;
|
||||||
background: #0f0f0f;
|
height: 70%;
|
||||||
border-radius: 16px;
|
background: #1a1a1a;
|
||||||
padding: 18px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
min-height: 0;
|
||||||
color: #f5f5f5;
|
overflow: hidden;
|
||||||
position: relative;
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 12px;
|
background: #2a2a2a;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-title {
|
.video-title {
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-meta {
|
.video-meta {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #b5b5b5;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-player {
|
.custom-player {
|
||||||
background: #000;
|
flex: 1;
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
justify-content: space-between;
|
||||||
|
min-height: 0;
|
||||||
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-element {
|
.video-element {
|
||||||
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 60vh;
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
object-fit: contain;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element:focus {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
background: #1c1c1c;
|
||||||
|
padding: 10px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 14px 14px;
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-controls {
|
.top-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-controls {
|
.left-controls {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn {
|
.control-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #f5f5f5;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.2s ease;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn:hover {
|
.control-btn:hover {
|
||||||
color: #f5b333;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn[disabled],
|
.control-btn[disabled] {
|
||||||
.control-btn[disabled]:hover {
|
opacity: 0.35;
|
||||||
color: rgba(245, 245, 245, 0.35);
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -1447,59 +1536,56 @@ async function openVideoAtIndex(index) {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider,
|
.volume-slider {
|
||||||
.progress-slider {
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
width: 100px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
#f5b333 var(--fill, 100%),
|
#ff3b30 calc(var(--fill, 100%) * 1%),
|
||||||
rgba(255, 255, 255, 0.24) var(--fill, 100%)
|
rgba(255, 255, 255, 0.3) calc(var(--fill, 100%) * 1%)
|
||||||
);
|
);
|
||||||
border-radius: 999px;
|
|
||||||
height: 4px;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider {
|
.volume-slider::-webkit-slider-thumb {
|
||||||
width: 90px;
|
-webkit-appearance: none;
|
||||||
}
|
|
||||||
|
|
||||||
.progress-slider {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.volume-slider::-webkit-slider-thumb,
|
|
||||||
.progress-slider::-webkit-slider-thumb {
|
|
||||||
appearance: none;
|
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f5b333;
|
background: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-top: -4px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-controls {
|
.bottom-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 13px;
|
flex-shrink: 0;
|
||||||
color: #d4d4d4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-close-btn {
|
.progress-slider {
|
||||||
position: absolute;
|
flex: 1;
|
||||||
top: 18px;
|
|
||||||
right: 28px;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
color: #f5f5f5;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 17px;
|
accent-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ body,
|
|||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(4px);
|
||||||
background: rgba(0, 0, 0, 0.8);
|
color: rgba(0, 0, 0, 0.35);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -420,8 +420,8 @@ body,
|
|||||||
.image-modal-overlay {
|
.image-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
531
server/server.js
531
server/server.js
@@ -2164,6 +2164,35 @@ function requireAuth(req, res, next) {
|
|||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
// --- Güvenli medya URL'i (TV için) ---
|
||||||
|
// Dönen URL segmentleri ayrı ayrı encode eder, slash'ları korur ve tam hostlu URL döner
|
||||||
|
app.get("/api/media-url", requireAuth, (req, res) => {
|
||||||
|
const filePath = req.query.path;
|
||||||
|
if (!filePath) return res.status(400).json({ error: "path parametresi gerekli" });
|
||||||
|
|
||||||
|
// TTL saniye olarak (default 3600 = 1 saat). Min 60s, max 72h
|
||||||
|
const ttl = Math.min(Math.max(Number(req.query.ttl) || 3600, 60), 72 * 3600);
|
||||||
|
|
||||||
|
// Medya token oluştur
|
||||||
|
const mediaToken = crypto.randomBytes(16).toString("hex");
|
||||||
|
activeTokens.add(mediaToken);
|
||||||
|
setTimeout(() => activeTokens.delete(mediaToken), ttl * 1000);
|
||||||
|
|
||||||
|
// Her path segmentini ayrı encode et (slash korunur)
|
||||||
|
const encodedPath = String(filePath)
|
||||||
|
.split(/[\\/]/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((s) => encodeURIComponent(s))
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
const host = req.get("host") || "localhost";
|
||||||
|
const protocol = req.protocol || (req.secure ? "https" : "http");
|
||||||
|
|
||||||
|
const absoluteUrl = `${protocol}://${host}/media/${encodedPath}?token=${mediaToken}`;
|
||||||
|
|
||||||
|
console.log("Generated media URL:", { original: filePath, url: absoluteUrl, ttl });
|
||||||
|
res.json({ url: absoluteUrl, token: mediaToken, expiresIn: ttl });
|
||||||
|
});
|
||||||
|
|
||||||
// --- Torrent veya magnet ekleme ---
|
// --- Torrent veya magnet ekleme ---
|
||||||
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||||
@@ -2381,6 +2410,26 @@ app.get("/movie-data/:path(*)", requireAuth, (req, res) => {
|
|||||||
const fullPath = resolveMovieDataAbsolute(relPath);
|
const fullPath = resolveMovieDataAbsolute(relPath);
|
||||||
if (!fullPath) return res.status(400).send("Geçersiz movie data yolu");
|
if (!fullPath) return res.status(400).send("Geçersiz movie data yolu");
|
||||||
if (!fs.existsSync(fullPath)) return res.status(404).send("Dosya bulunamadı");
|
if (!fs.existsSync(fullPath)) return res.status(404).send("Dosya bulunamadı");
|
||||||
|
|
||||||
|
// Cache kontrolü için dosya değişim zamanını ekle
|
||||||
|
const stats = fs.statSync(fullPath);
|
||||||
|
const lastModified = stats.mtime.getTime();
|
||||||
|
|
||||||
|
// Eğer client If-Modified-Since header gönderdiyse kontrol et
|
||||||
|
const ifModifiedSince = req.headers['if-modified-since'];
|
||||||
|
if (ifModifiedSince) {
|
||||||
|
const clientTime = new Date(ifModifiedSince).getTime();
|
||||||
|
if (clientTime >= lastModified) {
|
||||||
|
return res.status(304).end(); // Not Modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-Control header'larını ayarla
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Last-Modified', new Date(lastModified).toUTCString());
|
||||||
|
|
||||||
res.sendFile(fullPath);
|
res.sendFile(fullPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2389,6 +2438,26 @@ app.get("/tv-data/:path(*)", requireAuth, (req, res) => {
|
|||||||
const fullPath = resolveTvDataAbsolute(relPath);
|
const fullPath = resolveTvDataAbsolute(relPath);
|
||||||
if (!fullPath) return res.status(400).send("Geçersiz tv data yolu");
|
if (!fullPath) return res.status(400).send("Geçersiz tv data yolu");
|
||||||
if (!fs.existsSync(fullPath)) return res.status(404).send("Dosya bulunamadı");
|
if (!fs.existsSync(fullPath)) return res.status(404).send("Dosya bulunamadı");
|
||||||
|
|
||||||
|
// Cache kontrolü için dosya değişim zamanını ekle
|
||||||
|
const stats = fs.statSync(fullPath);
|
||||||
|
const lastModified = stats.mtime.getTime();
|
||||||
|
|
||||||
|
// Eğer client If-Modified-Since header gönderdiyse kontrol et
|
||||||
|
const ifModifiedSince = req.headers['if-modified-since'];
|
||||||
|
if (ifModifiedSince) {
|
||||||
|
const clientTime = new Date(ifModifiedSince).getTime();
|
||||||
|
if (clientTime >= lastModified) {
|
||||||
|
return res.status(304).end(); // Not Modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-Control header'larını ayarla
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Last-Modified', new Date(lastModified).toUTCString());
|
||||||
|
|
||||||
res.sendFile(fullPath);
|
res.sendFile(fullPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2638,9 +2707,28 @@ app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => {
|
|||||||
|
|
||||||
// --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) ---
|
// --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) ---
|
||||||
app.get("/media/:path(*)", requireAuth, (req, res) => {
|
app.get("/media/:path(*)", requireAuth, (req, res) => {
|
||||||
const relPath = req.params.path;
|
// URL'deki encode edilmiş karakterleri decode et
|
||||||
const fullPath = path.join(DOWNLOAD_DIR, relPath);
|
let relPath = req.params.path || "";
|
||||||
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
|
try {
|
||||||
|
relPath = decodeURIComponent(relPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to decode media path:", relPath, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeRelative sadece baştaki slash'ları temizler; buradan sonra ekstra kontrol yapıyoruz
|
||||||
|
const safeRel = sanitizeRelative(relPath);
|
||||||
|
if (!safeRel) {
|
||||||
|
console.error("Invalid media path after sanitize:", relPath);
|
||||||
|
return res.status(400).send("Invalid path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(DOWNLOAD_DIR, safeRel);
|
||||||
|
console.log("Media request:", { originalPath: req.params.path, decodedPath: relPath, safeRel, fullPath }); // Debug için log ekle
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
console.error("File not found:", fullPath);
|
||||||
|
return res.status(404).send("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -2648,6 +2736,13 @@ app.get("/media/:path(*)", requireAuth, (req, res) => {
|
|||||||
const isVideo = String(type).startsWith("video/");
|
const isVideo = String(type).startsWith("video/");
|
||||||
const range = req.headers.range;
|
const range = req.headers.range;
|
||||||
|
|
||||||
|
console.log("Media info:", { fileSize, type, isVideo, range }); // Debug için log ekle
|
||||||
|
|
||||||
|
// CORS headers ekle
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Range, Accept-Ranges, Content-Type");
|
||||||
|
|
||||||
if (isVideo && range) {
|
if (isVideo && range) {
|
||||||
const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
|
const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
|
||||||
const start = parseInt(startStr, 10);
|
const start = parseInt(startStr, 10);
|
||||||
@@ -2891,6 +2986,8 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
const seriesEpisodeInfo = relWithinRoot
|
const seriesEpisodeInfo = relWithinRoot
|
||||||
? info.seriesEpisodes?.[relWithinRoot] || null
|
? info.seriesEpisodes?.[relWithinRoot] || null
|
||||||
: null;
|
: null;
|
||||||
|
const isPrimaryVideo =
|
||||||
|
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
name: safeRel,
|
name: safeRel,
|
||||||
@@ -2908,6 +3005,7 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
mediaInfo: mediaInfoForFile,
|
mediaInfo: mediaInfoForFile,
|
||||||
primaryVideoPath: info.primaryVideoPath || null,
|
primaryVideoPath: info.primaryVideoPath || null,
|
||||||
primaryMediaInfo: info.primaryMediaInfo || null,
|
primaryMediaInfo: info.primaryMediaInfo || null,
|
||||||
|
movieMatch: isPrimaryVideo ? info.movieMatch || null : null,
|
||||||
seriesEpisode: seriesEpisodeInfo
|
seriesEpisode: seriesEpisodeInfo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3266,16 +3364,29 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
const relativeFile =
|
const relativeFile =
|
||||||
normalizedEpisode.file || normalizedEpisode.videoPath || "";
|
normalizedEpisode.file || normalizedEpisode.videoPath || "";
|
||||||
if (!normalizedEpisode.videoPath && relativeFile) {
|
const rawVideoPath = normalizedEpisode.videoPath || relativeFile || "";
|
||||||
const joined = relativeFile.includes("/")
|
let videoPath = rawVideoPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
||||||
? relativeFile
|
if (videoPath) {
|
||||||
: `${folder}/${relativeFile}`;
|
const isExternal = /^https?:\/\//i.test(videoPath);
|
||||||
normalizedEpisode.videoPath = joined.replace(/\\/g, "/");
|
const needsFolderPrefix =
|
||||||
} else if (normalizedEpisode.videoPath) {
|
!isExternal &&
|
||||||
normalizedEpisode.videoPath = normalizedEpisode.videoPath.replace(
|
!videoPath.startsWith(`${folder}/`) &&
|
||||||
/\\/g,
|
!videoPath.startsWith(`/${folder}/`);
|
||||||
"/"
|
if (needsFolderPrefix) {
|
||||||
);
|
videoPath = `${folder}/${videoPath}`.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
const finalPath = videoPath.replace(/^\/+/, "");
|
||||||
|
if (finalPath !== rawVideoPath) {
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
normalizedEpisode.videoPath = finalPath;
|
||||||
|
} else if (relativeFile) {
|
||||||
|
normalizedEpisode.videoPath = `${folder}/${relativeFile}`
|
||||||
|
.replace(/\\/g, "/")
|
||||||
|
.replace(/^\/+/, "");
|
||||||
|
if (normalizedEpisode.videoPath !== rawVideoPath) {
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
normalizedEpisode.folder = folder;
|
normalizedEpisode.folder = folder;
|
||||||
|
|
||||||
@@ -3502,7 +3613,7 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
|
|||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📂 Download path:", DOWNLOAD_DIR);
|
console.log("🗄️ Download path:", DOWNLOAD_DIR);
|
||||||
|
|
||||||
|
|
||||||
// --- ✅ Client build (frontend) dosyalarını sun ---
|
// --- ✅ Client build (frontend) dosyalarını sun ---
|
||||||
@@ -3516,7 +3627,7 @@ if (fs.existsSync(publicDir)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const server = app.listen(PORT, () =>
|
const server = app.listen(PORT, () =>
|
||||||
console.log(`✅ WebTorrent server ${PORT} portunda çalışıyor`)
|
console.log(`🐔 du.pe server ${PORT} portunda çalışıyor`)
|
||||||
);
|
);
|
||||||
|
|
||||||
wss = new WebSocketServer({ server });
|
wss = new WebSocketServer({ server });
|
||||||
@@ -3558,6 +3669,396 @@ app.get("/api/disk-space", requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- 🔍 TMDB/TVDB Arama Endpoint'i ---
|
||||||
|
app.get("/api/search/metadata", requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { query, year, type } = req.query;
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return res.status(400).json({ error: "query parametresi gerekli" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "movie") {
|
||||||
|
// TMDB Film Araması
|
||||||
|
if (!TMDB_API_KEY) {
|
||||||
|
return res.status(400).json({ error: "TMDB API key tanımlı değil" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
api_key: TMDB_API_KEY,
|
||||||
|
query: query,
|
||||||
|
language: "en-US",
|
||||||
|
include_adult: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
params.set("year", year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${TMDB_BASE_URL}/search/movie?${params}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`TMDB API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Her film için detaylı bilgi çek
|
||||||
|
const resultsWithDetails = await Promise.all(
|
||||||
|
(data.results || []).slice(0, 10).map(async (item) => {
|
||||||
|
try {
|
||||||
|
const detailResponse = await fetch(
|
||||||
|
`${TMDB_BASE_URL}/movie/${item.id}?api_key=${TMDB_API_KEY}&append_to_response=credits&language=en-US`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (detailResponse.ok) {
|
||||||
|
const details = await detailResponse.json();
|
||||||
|
const cast = (details.credits?.cast || []).slice(0, 3).map(c => c.name);
|
||||||
|
const genres = (details.genres || []).map(g => g.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
year: item.release_date ? item.release_date.slice(0, 4) : null,
|
||||||
|
overview: item.overview || "",
|
||||||
|
poster: item.poster_path ? `${TMDB_IMG_BASE}${item.poster_path}` : null,
|
||||||
|
runtime: details.runtime || null,
|
||||||
|
genres: genres,
|
||||||
|
cast: cast,
|
||||||
|
type: "movie"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ Film detayı alınamadı (${item.id}):`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
year: item.release_date ? item.release_date.slice(0, 4) : null,
|
||||||
|
overview: item.overview || "",
|
||||||
|
poster: item.poster_path ? `${TMDB_IMG_BASE}${item.poster_path}` : null,
|
||||||
|
type: "movie"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ results: resultsWithDetails });
|
||||||
|
} else if (type === "series") {
|
||||||
|
// TVDB Dizi Araması
|
||||||
|
if (!TVDB_API_KEY) {
|
||||||
|
return res.status(400).json({ error: "TVDB API key tanımlı değil" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ type: "series", query: query });
|
||||||
|
const resp = await tvdbFetch(`/search?${params.toString()}`);
|
||||||
|
|
||||||
|
if (!resp || !resp.data) {
|
||||||
|
return res.json({ results: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const allData = Array.isArray(resp.data) ? resp.data : [];
|
||||||
|
|
||||||
|
const resultsWithDetails = await Promise.all(
|
||||||
|
allData.slice(0, 20).map(async (item) => {
|
||||||
|
try {
|
||||||
|
const seriesId = item.tvdb_id || item.id;
|
||||||
|
const extended = await fetchTvdbSeriesExtended(seriesId);
|
||||||
|
|
||||||
|
if (extended) {
|
||||||
|
const info = extended.series || extended;
|
||||||
|
const artworks = Array.isArray(extended.artworks) ? extended.artworks : [];
|
||||||
|
const posterArtwork = artworks.find(a => {
|
||||||
|
const type = String(a?.type || a?.artworkType || "").toLowerCase();
|
||||||
|
return type.includes("poster") || type === "series" || type === "2";
|
||||||
|
});
|
||||||
|
|
||||||
|
const genres = Array.isArray(info.genres)
|
||||||
|
? info.genres.map(g => typeof g === "string" ? g : g?.name || g?.genre).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Yıl bilgisini çeşitli yerlerden al
|
||||||
|
let seriesYear = null;
|
||||||
|
if (info.year) {
|
||||||
|
seriesYear = Number(info.year);
|
||||||
|
} else if (item.year) {
|
||||||
|
seriesYear = Number(item.year);
|
||||||
|
} else if (info.first_air_date || info.firstAired) {
|
||||||
|
const dateStr = String(info.first_air_date || info.firstAired);
|
||||||
|
const yearMatch = dateStr.match(/(\d{4})/);
|
||||||
|
if (yearMatch) seriesYear = Number(yearMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: seriesId,
|
||||||
|
title: info.name || item.name,
|
||||||
|
year: seriesYear,
|
||||||
|
overview: info.overview || item.overview || "",
|
||||||
|
poster: posterArtwork?.image ? tvdbImageUrl(posterArtwork.image) : (item.image ? tvdbImageUrl(item.image) : null),
|
||||||
|
genres: genres,
|
||||||
|
status: info.status?.name || info.status || null,
|
||||||
|
type: "series"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ Dizi detayı alınamadı:`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback için yıl bilgisini al
|
||||||
|
let itemYear = null;
|
||||||
|
if (item.year) {
|
||||||
|
itemYear = Number(item.year);
|
||||||
|
} else if (item.first_air_date || item.firstAired) {
|
||||||
|
const dateStr = String(item.first_air_date || item.firstAired);
|
||||||
|
const yearMatch = dateStr.match(/(\d{4})/);
|
||||||
|
if (yearMatch) itemYear = Number(yearMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.tvdb_id || item.id,
|
||||||
|
title: item.name || item.seriesName,
|
||||||
|
year: itemYear,
|
||||||
|
overview: item.overview || "",
|
||||||
|
poster: item.image ? tvdbImageUrl(item.image) : null,
|
||||||
|
type: "series"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Yıl filtresi detaylı bilgiler alındıktan SONRA uygula
|
||||||
|
let filtered = resultsWithDetails.filter(Boolean);
|
||||||
|
if (year && year.trim()) {
|
||||||
|
const targetYear = Number(year);
|
||||||
|
console.log(`🔍 TVDB Yıl filtresi uygulanıyor: ${targetYear}`);
|
||||||
|
|
||||||
|
filtered = filtered.filter(item => {
|
||||||
|
const itemYear = item.year ? Number(item.year) : null;
|
||||||
|
const matches = itemYear && itemYear === targetYear;
|
||||||
|
console.log(` - ${item.title}: yıl=${itemYear}, eşleşme=${matches}`);
|
||||||
|
return matches;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🔍 Yıl filtresinden sonra: ${filtered.length} sonuç`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ results: filtered.slice(0, 10) });
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: "type parametresi 'movie' veya 'series' olmalı" });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Metadata search error:", err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 🔗 Manuel Eşleştirme Endpoint'i ---
|
||||||
|
app.post("/api/match/manual", requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filePath, metadata, type, season, episode } = req.body;
|
||||||
|
|
||||||
|
if (!filePath || !metadata || !type) {
|
||||||
|
return res.status(400).json({ error: "filePath, metadata ve type gerekli" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const safePath = sanitizeRelative(filePath);
|
||||||
|
if (!safePath) {
|
||||||
|
return res.status(400).json({ error: "Geçersiz dosya yolu" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(DOWNLOAD_DIR, safePath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return res.status(404).json({ error: "Dosya bulunamadı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootFolder = rootFromRelPath(safePath);
|
||||||
|
if (!rootFolder) {
|
||||||
|
return res.status(400).json({ error: "Kök klasör belirlenemedi" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootDir = path.join(DOWNLOAD_DIR, rootFolder);
|
||||||
|
const infoPath = infoFilePath(rootDir);
|
||||||
|
|
||||||
|
// Mevcut info.json dosyasını oku
|
||||||
|
let infoData = {};
|
||||||
|
if (fs.existsSync(infoPath)) {
|
||||||
|
try {
|
||||||
|
infoData = JSON.parse(fs.readFileSync(infoPath, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ info.json okunamadı (${infoPath}): ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media info'yu çıkar
|
||||||
|
let mediaInfo = null;
|
||||||
|
try {
|
||||||
|
mediaInfo = await extractMediaInfo(fullPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ Media info alınamadı (${fullPath}): ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Önce mevcut verileri temizle
|
||||||
|
if (type === "movie") {
|
||||||
|
// Film işlemleri
|
||||||
|
const movieId = metadata.id;
|
||||||
|
if (!movieId) {
|
||||||
|
return res.status(400).json({ error: "Film ID bulunamadı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mevcut movie_data ve TV verilerini temizle
|
||||||
|
removeMovieData(rootFolder);
|
||||||
|
removeSeriesData(rootFolder);
|
||||||
|
|
||||||
|
// TMDB'den detaylı bilgi al
|
||||||
|
const movieDetails = await tmdbFetch(`/movie/${movieId}`, {
|
||||||
|
language: "en-US",
|
||||||
|
append_to_response: "release_dates,credits,translations"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!movieDetails) {
|
||||||
|
return res.status(400).json({ error: "Film detayları alınamadı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Türkçe çevirileri ekle
|
||||||
|
if (movieDetails.translations?.translations?.length) {
|
||||||
|
const translations = movieDetails.translations.translations;
|
||||||
|
const turkish = translations.find(
|
||||||
|
(t) => t.iso_639_1 === "tr" && t.data
|
||||||
|
);
|
||||||
|
if (turkish?.data) {
|
||||||
|
const data = turkish.data;
|
||||||
|
if (data.overview) movieDetails.overview = data.overview;
|
||||||
|
if (data.title) movieDetails.title = data.title;
|
||||||
|
if (data.tagline) movieDetails.tagline = data.tagline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movie data'yı kaydet
|
||||||
|
const movieDataResult = await ensureMovieData(
|
||||||
|
rootFolder,
|
||||||
|
metadata.title,
|
||||||
|
safePath.split('/').slice(1).join('/'),
|
||||||
|
mediaInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (movieDataResult) {
|
||||||
|
// info.json'u güncelle - eski verileri temizle
|
||||||
|
infoData.primaryVideoPath = safePath.split('/').slice(1).join('/');
|
||||||
|
infoData.primaryMediaInfo = movieDataResult;
|
||||||
|
infoData.movieMatch = {
|
||||||
|
id: movieDetails.id,
|
||||||
|
title: movieDetails.title,
|
||||||
|
year: movieDetails.release_date ? movieDetails.release_date.slice(0, 4) : null,
|
||||||
|
poster: movieDetails.poster_path,
|
||||||
|
backdrop: movieDetails.backdrop_path,
|
||||||
|
matchedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eski dizi verilerini temizle
|
||||||
|
delete infoData.seriesEpisodes;
|
||||||
|
|
||||||
|
upsertInfoFile(rootDir, infoData);
|
||||||
|
}
|
||||||
|
} else if (type === "series") {
|
||||||
|
// Dizi işlemleri
|
||||||
|
if (season === null || episode === null) {
|
||||||
|
return res.status(400).json({ error: "Dizi için sezon ve bölüm bilgileri gerekli" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesId = metadata.id;
|
||||||
|
if (!seriesId) {
|
||||||
|
return res.status(400).json({ error: "Dizi ID bulunamadı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mevcut movie_data ve TV verilerini temizle
|
||||||
|
removeMovieData(rootFolder);
|
||||||
|
removeSeriesData(rootFolder);
|
||||||
|
|
||||||
|
// TVDB'den dizi bilgilerini al
|
||||||
|
const extended = await fetchTvdbSeriesExtended(seriesId);
|
||||||
|
if (!extended) {
|
||||||
|
return res.status(400).json({ error: "Dizi detayları alınamadı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dizi bilgilerini oluştur
|
||||||
|
const seriesInfo = {
|
||||||
|
title: metadata.title,
|
||||||
|
searchTitle: metadata.title,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
key: `S${String(season).padStart(2, "0")}E${String(episode).padStart(2, "0")}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// TV data'yı kaydet
|
||||||
|
const tvDataResult = await ensureSeriesData(
|
||||||
|
rootFolder,
|
||||||
|
safePath.split('/').slice(1).join('/'),
|
||||||
|
seriesInfo,
|
||||||
|
mediaInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tvDataResult) {
|
||||||
|
// info.json'u güncelle - eski verileri temizle
|
||||||
|
if (!infoData.seriesEpisodes) infoData.seriesEpisodes = {};
|
||||||
|
|
||||||
|
const relPath = safePath.split('/').slice(1).join('/');
|
||||||
|
infoData.seriesEpisodes[relPath] = {
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
key: seriesInfo.key,
|
||||||
|
title: tvDataResult.episode.title || seriesInfo.title,
|
||||||
|
showId: tvDataResult.show.id || null,
|
||||||
|
showTitle: tvDataResult.show.title || seriesInfo.title,
|
||||||
|
seasonName: tvDataResult.season?.name || `Season ${season}`,
|
||||||
|
seasonId: tvDataResult.season?.tvdbSeasonId || null,
|
||||||
|
seasonPoster: tvDataResult.season?.poster || null,
|
||||||
|
overview: tvDataResult.episode.overview || "",
|
||||||
|
aired: tvDataResult.episode.aired || null,
|
||||||
|
runtime: tvDataResult.episode.runtime || null,
|
||||||
|
still: tvDataResult.episode.still || null,
|
||||||
|
episodeId: tvDataResult.episode.tvdbEpisodeId || null,
|
||||||
|
slug: tvDataResult.episode.slug || null,
|
||||||
|
matchedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eski film verilerini temizle
|
||||||
|
delete infoData.movieMatch;
|
||||||
|
delete infoData.primaryMediaInfo;
|
||||||
|
|
||||||
|
upsertInfoFile(rootDir, infoData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail'ı yeniden oluştur
|
||||||
|
if (mediaInfo?.format?.mimeType?.startsWith("video/")) {
|
||||||
|
queueVideoThumbnail(fullPath, safePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Değişiklikleri bildir
|
||||||
|
broadcastFileUpdate(rootFolder);
|
||||||
|
|
||||||
|
// Elle eşleştirme için özel bildirim gönder
|
||||||
|
if (wss) {
|
||||||
|
const data = JSON.stringify({
|
||||||
|
type: "manualMatch",
|
||||||
|
filePath: safePath,
|
||||||
|
rootFolder,
|
||||||
|
matchType: type
|
||||||
|
});
|
||||||
|
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Eşleştirme başarıyla tamamlandı",
|
||||||
|
type,
|
||||||
|
rootFolder
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Manual match error:", err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
if (!String(err).includes("uTP"))
|
if (!String(err).includes("uTP"))
|
||||||
console.error("WebTorrent error:", err.message);
|
console.error("WebTorrent error:", err.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user