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

@@ -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(/&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() {
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;