feat(turkanime): turkanime bölüm tarama özelliği ekle

Turkanime anime sayfalarından bölüm listesini çekmek için yeni API uç
noktası ve kullanıcı arayüzü desteği eklendi. Kullanıcılar artık
Turkanime URL'lerini girerek bölümleri listeleyebilir ve
TURKANIME_DEBUG ortam değişkeni ile detaylı logları aktif edebilir.
This commit is contained in:
2026-01-22 16:21:55 +03:00
parent 1bad4f7256
commit 081772b615
4 changed files with 383 additions and 2 deletions

View File

@@ -31,3 +31,5 @@ AUTO_PAUSE_ON_COMPLETE=0
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır; # Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır. # CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
DISABLE_MEDIA_PROCESSING=0 DISABLE_MEDIA_PROCESSING=0
# Turkanime bölüm taraması için detaylı logları açar.
TURKANIME_DEBUG=0

View File

@@ -98,8 +98,25 @@
} }
} }
function normalizeTurkanimeAnimeUrl(value) {
if (!value || typeof value !== "string") return null;
try {
const url = new URL(value.trim());
const host = url.hostname.toLowerCase();
if (host !== "turkanime.tv" && host !== "www.turkanime.tv") return null;
const pathname = url.pathname.replace(/\/+$/, "");
const match = pathname.match(/^\/anime\/([^/]+)$/);
if (!match) return null;
const slug = match[1]?.trim();
if (!slug) return null;
return `https://www.turkanime.tv/anime/${slug}`;
} catch {
return null;
}
}
async function handleUrlInput() { async function handleUrlInput() {
const input = prompt("Magnet veya YouTube URL girin:"); const input = prompt("Magnet, YouTube veya Turkanime URL girin:");
if (!input) return; if (!input) return;
if (isMagnetLink(input)) { if (isMagnetLink(input)) {
await apiFetch("/api/transfer", { await apiFetch("/api/transfer", {
@@ -125,8 +142,29 @@
await list(); await list();
return; return;
} }
const normalizedTurkanime = normalizeTurkanimeAnimeUrl(input);
if (normalizedTurkanime) {
const resp = await apiFetch("/api/turkanime/episodes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: normalizedTurkanime })
});
if (!resp.ok) {
const data = await resp.json().catch(() => null);
alert(data?.error || "Turkanime bölümleri getirilemedi.");
return;
}
const data = await resp.json().catch(() => null);
const episodes = data?.episodes || [];
if (!episodes.length) {
alert("Turkanime için bölüm bulunamadı.");
return;
}
alert(`Toplam ${data.count} bölüm bulundu:\n\n${episodes.join("\n")}`);
return;
}
alert( alert(
"Yalnızca magnet linkleri veya https://www.youtube.com/watch?v=... formatındaki YouTube URL'leri destekleniyor." "Yalnızca magnet linkleri, https://www.youtube.com/watch?v=... formatındaki YouTube URL'leri veya https://www.turkanime.tv/anime/... formatındaki Turkanime URL'leri destekleniyor."
); );
} }

View File

@@ -19,3 +19,4 @@ services:
DEBUG_CPU: ${DEBUG_CPU} DEBUG_CPU: ${DEBUG_CPU}
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE} AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING} DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}
TURKANIME_DEBUG: ${TURKANIME_DEBUG}

View File

@@ -17,6 +17,27 @@ import { createWebsocketServer, broadcastJson } from "./modules/websocket.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const ROOT_ENV_PATH = path.join(__dirname, "..", ".env");
if (fs.existsSync(ROOT_ENV_PATH)) {
const envLines = fs.readFileSync(ROOT_ENV_PATH, "utf-8").split(/\r?\n/);
for (const line of envLines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIndex = trimmed.indexOf("=");
if (eqIndex <= 0) continue;
const key = trimmed.slice(0, eqIndex).trim();
if (!key || Object.prototype.hasOwnProperty.call(process.env, key)) continue;
let value = trimmed.slice(eqIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}
const app = express(); const app = express();
const upload = multer({ dest: path.join(__dirname, "uploads") }); const upload = multer({ dest: path.join(__dirname, "uploads") });
const client = new WebTorrent(); const client = new WebTorrent();
@@ -92,6 +113,12 @@ const YT_ALLOWED_RESOLUTIONS = new Set([
const YT_EXTRACTOR_ARGS = const YT_EXTRACTOR_ARGS =
process.env.YT_DLP_EXTRACTOR_ARGS || null; process.env.YT_DLP_EXTRACTOR_ARGS || null;
let resolvedYtDlpBinary = null; let resolvedYtDlpBinary = null;
const TURKANIME_HOSTS = new Set(["turkanime.tv", "www.turkanime.tv"]);
const TURKANIME_MAX_EPISODES =
Number(process.env.TURKANIME_MAX_EPISODES) > 0
? Number(process.env.TURKANIME_MAX_EPISODES)
: 500;
const TURKANIME_DEBUG = process.env.TURKANIME_DEBUG === "1";
const TMDB_API_KEY = process.env.TMDB_API_KEY; const TMDB_API_KEY = process.env.TMDB_API_KEY;
const TMDB_BASE_URL = "https://api.themoviedb.org/3"; const TMDB_BASE_URL = "https://api.themoviedb.org/3";
const TMDB_IMG_BASE = const TMDB_IMG_BASE =
@@ -111,6 +138,185 @@ const FFPROBE_MAX_BUFFER =
: 10 * 1024 * 1024; : 10 * 1024 * 1024;
const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png"); const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png");
function parseTurkanimeSlug(rawUrl) {
if (!rawUrl || typeof rawUrl !== "string") return null;
try {
const url = new URL(rawUrl.trim());
const host = url.hostname.toLowerCase();
if (!TURKANIME_HOSTS.has(host)) return null;
const pathname = url.pathname.replace(/\/+$/, "");
const match = pathname.match(/^\/anime\/([^/]+)$/);
if (!match) return null;
const slug = match[1]?.trim();
return slug ? slug.toLowerCase() : null;
} catch {
return null;
}
}
function logTurkanime(message) {
if (!TURKANIME_DEBUG) return;
console.log(`Turkanime: ${message}`);
}
function isTurkanimeNotFound(html) {
if (!html) return false;
return (
/404\s*Sayfa\s*Bulunamadı/i.test(html) ||
/Sayfa\s*Bulunamadı/i.test(html) ||
/Sayfa\s*bulunamadı/i.test(html) ||
/\b404\b/.test(html)
);
}
function extractTurkanimeEpisodeLinks(html, slug) {
if (!html || !slug) return [];
const links = new Set();
const normalizedHtml = html
.replace(/\\u002F/g, "/")
.replace(/\\\//g, "/")
.replace(/\\"/g, '"')
.replace(/&quot;/g, '"')
.replace(/&#34;/g, '"');
const normalizeTurkanimeUrl = (value) => {
if (!value) return null;
let url = value.trim();
if (url.startsWith("//")) url = `https:${url}`;
url = url.replace(/^https?:\/\//, "https://");
if (url.startsWith("/video/")) {
url = `https://www.turkanime.tv${url}`;
}
return url.replace(/\/+$/, "");
};
const hrefRegex =
/href\s*=\s*["'](?:(https?:)?\/\/www\.turkanime\.tv)?(\/video\/[a-z0-9-]+)["']/gi;
let match;
while ((match = hrefRegex.exec(normalizedHtml))) {
const pathPart = match[2];
if (!pathPart.includes(slug)) continue;
const isSingle = pathPart.endsWith(`/video/${slug}`);
if (!pathPart.includes("-bolum") && !isSingle) continue;
const normalized = normalizeTurkanimeUrl(pathPart);
if (normalized) links.add(normalized);
}
// href dışında geçen tam/protokol-relative linkleri de yakala (JSON/JS/attribute)
const urlRegex =
/(https?:)?\/\/www\.turkanime\.tv\/video\/[a-z0-9-]+/gi;
while ((match = urlRegex.exec(normalizedHtml))) {
const normalized = normalizeTurkanimeUrl(match[0]);
if (!normalized) continue;
if (!normalized.includes(slug)) continue;
const isSingle = normalized.endsWith(`/video/${slug}`);
if (!normalized.includes("-bolum") && !isSingle) continue;
links.add(normalized);
}
// /video/ linkleri yoksa slug tabanlı string'leri yakala (JSON/JS içinde olabilir)
const escapedSlug = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const slugRegex = new RegExp(
`${escapedSlug}-\\d+-bolum(?:-final)?`,
"gi"
);
while ((match = slugRegex.exec(normalizedHtml))) {
const slugMatch = match[0].replace(/\/+$/, "");
const normalized = normalizeTurkanimeUrl(`/video/${slugMatch}`);
if (normalized) links.add(normalized);
}
return Array.from(links);
}
function isTurkanimeSingleEpisodeUrl(url, slug) {
return !!url && !!slug && url.endsWith(`/video/${slug}`);
}
function getTurkanimeEpisodeNumber(url, slug) {
const match = url.match(/-(\d+)-bolum/i);
if (!match) return null;
return Number(match[1]);
}
async function fetchTurkanimePage(url) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const resp = await fetch(url, {
method: "GET",
redirect: "follow",
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
Accept: "text/html"
}
});
const status = resp.status;
const html = await resp.text();
const notFound = isTurkanimeNotFound(html);
return {
status,
notFound,
ok: resp.ok && !notFound,
html,
url
};
} finally {
clearTimeout(timeout);
}
}
function extractTurkanimeAnimeId(html) {
if (!html) return null;
const patterns = [
/animeId\s*[:=]\s*["']?(\d+)["']?/i,
/anime_id\s*[:=]\s*["']?(\d+)["']?/i,
/data-anime-id\s*=\s*["'](\d+)["']/i,
/data-animeid\s*=\s*["'](\d+)["']/i
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match && match[1]) return match[1];
}
return null;
}
async function fetchTurkanimeAjaxEpisodes(animeId, referer) {
const urls = [
`https://www.turkanime.tv/ajax/bolumler&animeId=${animeId}`,
`https://www.turkanime.tv/ajax/bolumler?animeId=${animeId}`
];
let lastResult = null;
for (const url of urls) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const resp = await fetch(url, {
method: "GET",
redirect: "follow",
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
Accept: "text/html",
"X-Requested-With": "XMLHttpRequest",
Referer: referer || "https://www.turkanime.tv/"
}
});
const html = await resp.text();
lastResult = { status: resp.status, html, url };
if (resp.status === 200 && html && html.trim().length > 0) {
return lastResult;
}
} finally {
clearTimeout(timeout);
}
}
return lastResult;
}
function getWsClientCount() { function getWsClientCount() {
if (!wss) return 0; if (!wss) return 0;
let count = 0; let count = 0;
@@ -6216,6 +6422,140 @@ app.post("/api/movies/rescan", requireAuth, async (req, res) => {
} }
}); });
app.post("/api/turkanime/episodes", requireAuth, async (req, res) => {
const rawUrl = req.body?.url;
const slug = parseTurkanimeSlug(rawUrl);
if (!slug) {
return res.status(400).json({
ok: false,
error: "Geçerli bir Turkanime anime URL'si gerekli."
});
}
const episodes = [];
let lastUrl = null;
let lastStatus = null;
logTurkanime(`${slug} animesine ait linkler bulunuyor.`);
let animePage;
try {
animePage = await fetchTurkanimePage(
`https://www.turkanime.tv/anime/${slug}`
);
} catch (err) {
logTurkanime(
`Anime sayfası okunamadı, fallback taramaya geçiliyor.`
);
}
if (animePage?.ok) {
let extracted = extractTurkanimeEpisodeLinks(animePage.html, slug);
logTurkanime(`Anime sayfasından ${extracted.length} link ayıklandı.`);
if (!extracted.length) {
const animeId = extractTurkanimeAnimeId(animePage.html);
if (animeId) {
logTurkanime(`animeId bulundu: ${animeId}`);
try {
const ajaxPage = await fetchTurkanimeAjaxEpisodes(
animeId,
`https://www.turkanime.tv/anime/${slug}`
);
if (ajaxPage?.html) {
extracted = extractTurkanimeEpisodeLinks(ajaxPage.html, slug);
logTurkanime(
`Ajax bolum listesi içinden ${extracted.length} link ayıklandı.`
);
if (TURKANIME_DEBUG) {
const len = ajaxPage.html.length;
const hasSlug = ajaxPage.html.includes(slug);
logTurkanime(
`Ajax response: status ${ajaxPage.status}, length ${len}, slug var mı: ${hasSlug ? "evet" : "hayır"}`
);
if (!extracted.length) {
const sample = [];
const sampleRegex =
/(https?:)?\/\/www\.turkanime\.tv\/video\/[a-z0-9-]+/gi;
let sm;
while ((sm = sampleRegex.exec(ajaxPage.html)) && sample.length < 5) {
sample.push(sm[0]);
}
if (sample.length) {
logTurkanime(`Ajax örnek linkler: ${sample.join(", ")}`);
}
}
}
} else {
logTurkanime(
`Ajax bolum listesi başarısız. Status: ${ajaxPage?.status}`
);
}
} catch (err) {
logTurkanime(
`Ajax bolum listesi okunamadı: ${err?.message || err}`
);
}
} else {
logTurkanime(`animeId bulunamadı, ajax listesi denemesi atlandı.`);
}
}
if (!extracted.length && TURKANIME_DEBUG) {
const hasVideo = animePage.html.includes("/video/");
const sampleMatches = [];
const sampleRegex = /\/video\/[a-z0-9-]+/gi;
let m;
while ((m = sampleRegex.exec(animePage.html)) && sampleMatches.length < 5) {
sampleMatches.push(m[0]);
}
logTurkanime(
`Ham HTML'de /video/ var mı: ${hasVideo ? "evet" : "hayır"}`
);
if (sampleMatches.length) {
logTurkanime(
`Ham örnek linkler: ${sampleMatches.join(", ")}`
);
}
}
if (extracted.length) {
extracted.sort((a, b) => {
const aNum =
getTurkanimeEpisodeNumber(a, slug) ??
(isTurkanimeSingleEpisodeUrl(a, slug) ? 1 : 0);
const bNum =
getTurkanimeEpisodeNumber(b, slug) ??
(isTurkanimeSingleEpisodeUrl(b, slug) ? 1 : 0);
if (aNum !== bNum) return aNum - bNum;
const aFinal = /-bolum-final$/i.test(a);
const bFinal = /-bolum-final$/i.test(b);
if (aFinal === bFinal) return a.localeCompare(b);
return aFinal ? 1 : -1;
});
for (const url of extracted) {
const num =
getTurkanimeEpisodeNumber(url, slug) ??
(isTurkanimeSingleEpisodeUrl(url, slug) ? 1 : null);
if (num) {
const suffix = /-bolum-final$/i.test(url) ? "-bolum-final" : "-bolum";
logTurkanime(`${num}${suffix} bulundu`);
}
episodes.push(url);
}
lastUrl = extracted[extracted.length - 1];
lastStatus = "listed";
logTurkanime(`Son kontrol edilen link: ${lastUrl} (status ${lastStatus})`);
logTurkanime(`Toplam ${episodes.length} bolum bulundu`);
return res.json({ ok: true, slug, count: episodes.length, episodes });
}
}
if (lastUrl) {
logTurkanime(`Son kontrol edilen link: ${lastUrl} (status ${lastStatus})`);
}
logTurkanime(`Toplam ${episodes.length} bolum bulundu`);
res.json({ ok: true, slug, count: episodes.length, episodes });
});
app.post("/api/youtube/download", requireAuth, async (req, res) => { app.post("/api/youtube/download", requireAuth, async (req, res) => {
try { try {
const rawUrl = req.body?.url; const rawUrl = req.body?.url;