Dizi ve filmleri eşleştirme modu eklendi.

This commit is contained in:
2025-10-29 23:58:55 +03:00
parent 279adf12e9
commit 8d7605969b
5 changed files with 1574 additions and 214 deletions

View File

@@ -2164,6 +2164,35 @@ function requireAuth(req, res, next) {
return res.status(401).json({ error: "Unauthorized" });
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 ---
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);
if (!fullPath) return res.status(400).send("Geçersiz movie data yolu");
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);
});
@@ -2389,6 +2438,26 @@ app.get("/tv-data/:path(*)", requireAuth, (req, res) => {
const fullPath = resolveTvDataAbsolute(relPath);
if (!fullPath) return res.status(400).send("Geçersiz tv data yolu");
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);
});
@@ -2638,9 +2707,28 @@ app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => {
// --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) ---
app.get("/media/:path(*)", requireAuth, (req, res) => {
const relPath = req.params.path;
const fullPath = path.join(DOWNLOAD_DIR, relPath);
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
// URL'deki encode edilmiş karakterleri decode et
let relPath = req.params.path || "";
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 fileSize = stat.size;
@@ -2648,6 +2736,13 @@ app.get("/media/:path(*)", requireAuth, (req, res) => {
const isVideo = String(type).startsWith("video/");
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) {
const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
const start = parseInt(startStr, 10);
@@ -2891,6 +2986,8 @@ app.get("/api/files", requireAuth, (req, res) => {
const seriesEpisodeInfo = relWithinRoot
? info.seriesEpisodes?.[relWithinRoot] || null
: null;
const isPrimaryVideo =
!!info.primaryVideoPath && info.primaryVideoPath === relWithinRoot;
result.push({
name: safeRel,
@@ -2908,6 +3005,7 @@ app.get("/api/files", requireAuth, (req, res) => {
mediaInfo: mediaInfoForFile,
primaryVideoPath: info.primaryVideoPath || null,
primaryMediaInfo: info.primaryMediaInfo || null,
movieMatch: isPrimaryVideo ? info.movieMatch || null : null,
seriesEpisode: seriesEpisodeInfo
});
}
@@ -3266,16 +3364,29 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
}
const relativeFile =
normalizedEpisode.file || normalizedEpisode.videoPath || "";
if (!normalizedEpisode.videoPath && relativeFile) {
const joined = relativeFile.includes("/")
? relativeFile
: `${folder}/${relativeFile}`;
normalizedEpisode.videoPath = joined.replace(/\\/g, "/");
} else if (normalizedEpisode.videoPath) {
normalizedEpisode.videoPath = normalizedEpisode.videoPath.replace(
/\\/g,
"/"
);
const rawVideoPath = normalizedEpisode.videoPath || relativeFile || "";
let videoPath = rawVideoPath.replace(/\\/g, "/").replace(/^\.\//, "");
if (videoPath) {
const isExternal = /^https?:\/\//i.test(videoPath);
const needsFolderPrefix =
!isExternal &&
!videoPath.startsWith(`${folder}/`) &&
!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;
@@ -3502,7 +3613,7 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
stream.pipe(res);
});
console.log("📂 Download path:", DOWNLOAD_DIR);
console.log("🗄️ Download path:", DOWNLOAD_DIR);
// --- ✅ Client build (frontend) dosyalarını sun ---
@@ -3516,7 +3627,7 @@ if (fs.existsSync(publicDir)) {
}
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 });
@@ -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) => {
if (!String(err).includes("uTP"))
console.error("WebTorrent error:", err.message);