From 081772b6159a163f813e5d27440543ba8de3ab15 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Thu, 22 Jan 2026 16:21:55 +0300 Subject: [PATCH] =?UTF-8?q?feat(turkanime):=20turkanime=20b=C3=B6l=C3=BCm?= =?UTF-8?q?=20tarama=20=C3=B6zelli=C4=9Fi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .env.example | 2 + client/src/routes/Transfers.svelte | 42 +++- docker-compose.yml | 1 + server/server.js | 340 +++++++++++++++++++++++++++++ 4 files changed, 383 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 02f69d4..9a45882 100644 --- a/.env.example +++ b/.env.example @@ -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; # CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır. DISABLE_MEDIA_PROCESSING=0 +# Turkanime bölüm taraması için detaylı logları açar. +TURKANIME_DEBUG=0 diff --git a/client/src/routes/Transfers.svelte b/client/src/routes/Transfers.svelte index b6e6c14..86c322d 100644 --- a/client/src/routes/Transfers.svelte +++ b/client/src/routes/Transfers.svelte @@ -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() { - const input = prompt("Magnet veya YouTube URL girin:"); + const input = prompt("Magnet, YouTube veya Turkanime URL girin:"); if (!input) return; if (isMagnetLink(input)) { await apiFetch("/api/transfer", { @@ -125,8 +142,29 @@ await list(); 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( - "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." ); } diff --git a/docker-compose.yml b/docker-compose.yml index c549a27..02834da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,3 +19,4 @@ services: DEBUG_CPU: ${DEBUG_CPU} AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE} DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING} + TURKANIME_DEBUG: ${TURKANIME_DEBUG} diff --git a/server/server.js b/server/server.js index a37bb75..329e3d9 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,27 @@ import { createWebsocketServer, broadcastJson } from "./modules/websocket.js"; const __filename = fileURLToPath(import.meta.url); 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 upload = multer({ dest: path.join(__dirname, "uploads") }); const client = new WebTorrent(); @@ -92,6 +113,12 @@ const YT_ALLOWED_RESOLUTIONS = new Set([ const YT_EXTRACTOR_ARGS = process.env.YT_DLP_EXTRACTOR_ARGS || 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_BASE_URL = "https://api.themoviedb.org/3"; const TMDB_IMG_BASE = @@ -111,6 +138,185 @@ const FFPROBE_MAX_BUFFER = : 10 * 1024 * 1024; 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(/"/g, '"') + .replace(/"/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() { if (!wss) return 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) => { try { const rawUrl = req.body?.url;