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:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
340
server/server.js
340
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;
|
||||
|
||||
Reference in New Issue
Block a user