feat(ui): mail.ru linkleri için eşleştirme ve isim düzenlemesi eklendi

- Dosya eşleştirme arayüzü bağımsız `MatchModal` bileşenine taşındı
- `Files.svelte` ve `Transfers.svelte` yeni bileşen kullanılarak güncellendi
- Mail.ru indirmeleri için dizi adı, sezon ve bölüm eşleştirme özelliği eklendi
- `POST /api/mailru/match` endpointi ile metadata eşleştirme backend desteği sağlandı
- Dosya isimleri "DiziAdi.S01E01.mp4" formatında kaydedilmeye başlandı
This commit is contained in:
2026-01-26 21:22:15 +03:00
parent 0b99fce5a9
commit 52bd325dc6
5 changed files with 1010 additions and 543 deletions

View File

@@ -9,4 +9,6 @@ srt
txt
vtt
info.json
.trash
.trash
.aria2
mail_ru_gereksiz_dosya.png

View File

@@ -1188,6 +1188,29 @@ function sanitizeFileName(name) {
return replaced || "mailru_video.mp4";
}
function formatMailRuSeriesFilename(title, season, episode) {
const rawTitle = String(title || "").trim();
const parts = rawTitle
.replace(/[\\/:*?"<>|]+/g, "")
.replace(/[\s._-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.split(" ")
.filter(Boolean);
const titled = parts
.map((word) => {
if (!word) return "";
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.filter(Boolean)
.join(".");
const safeTitle = titled || "Anime";
const seasonNum = Number(season) || 1;
const episodeNum = Number(episode) || 1;
const code = `S${String(seasonNum).padStart(2, "0")}xE${String(episodeNum).padStart(2, "0")}`;
return sanitizeFileName(`${safeTitle}.${code}.mp4`);
}
async function resolveMailRuDirectUrl(rawUrl) {
const normalized = normalizeMailRuUrl(rawUrl);
if (!normalized) return null;
@@ -1411,11 +1434,11 @@ function finalizeMailRuJob(job, exitCode) {
job.progress = 1;
job.state = "completed";
job.error = null;
const relPath = path
.join(job.folderId, job.fileName)
.replace(/\\/g, "/");
const relPath = job.savePath === DOWNLOAD_DIR
? String(job.fileName || "")
: path.join(job.folderId || "", job.fileName || "").replace(/\\/g, "/");
attachMailRuThumbnail(job, filePath, relPath);
broadcastFileUpdate(job.folderId);
broadcastFileUpdate(relPath || "downloads");
scheduleSnapshotBroadcast();
broadcastDiskSpace();
console.log(`✅ Mail.ru indirmesi tamamlandı: ${job.title}`);
@@ -1493,13 +1516,13 @@ function launchMailRuJob(job) {
async function startMailRuDownload(url) {
const normalized = normalizeMailRuUrl(url);
if (!normalized) return null;
const folderId = `mailru_${Date.now().toString(36)}`;
const savePath = path.join(DOWNLOAD_DIR, folderId);
fs.mkdirSync(savePath, { recursive: true });
const folderId = null;
const savePath = DOWNLOAD_DIR;
const jobId = `mailru_${Date.now().toString(36)}`;
const job = {
id: folderId,
infoHash: folderId,
id: jobId,
infoHash: jobId,
type: "mailru",
url: normalized,
directUrl: null,
@@ -1508,7 +1531,7 @@ async function startMailRuDownload(url) {
added: Date.now(),
title: null,
fileName: null,
state: "resolving",
state: "awaiting_match",
progress: 0,
downloaded: 0,
totalBytes: 0,
@@ -1518,24 +1541,35 @@ async function startMailRuDownload(url) {
thumbnail: null,
process: null,
error: null,
debug: { binary: null, args: null, logs: [] }
debug: { binary: null, args: null, logs: [] },
match: null
};
mailruJobs.set(job.id, job);
scheduleSnapshotBroadcast();
console.log(`▶️ Mail.ru indirmesi başlatıldı: ${job.url}`);
console.log(`▶️ Mail.ru indirimi eşleştirme bekliyor: ${job.url}`);
return job;
}
async function beginMailRuDownload(job) {
if (!job || job.state !== "awaiting_match") return false;
job.state = "resolving";
scheduleSnapshotBroadcast();
try {
const directUrl = await resolveMailRuDirectUrl(normalized);
const directUrl = await resolveMailRuDirectUrl(job.url);
if (!directUrl) {
throw new Error("Mail.ru video URL'si çözümlenemedi");
}
job.directUrl = directUrl;
const urlObj = new URL(directUrl);
const filename = sanitizeFileName(path.basename(urlObj.pathname));
job.fileName = filename || `mailru_${Date.now()}.mp4`;
if (!job.fileName) {
const urlObj = new URL(directUrl);
const filename = sanitizeFileName(path.basename(urlObj.pathname));
job.fileName = filename || `mailru_${Date.now()}.mp4`;
}
job.title = job.fileName;
job.state = "downloading";
scheduleSnapshotBroadcast();
try {
const headResp = await fetch(directUrl, { method: "HEAD" });
const length = Number(headResp.headers.get("content-length")) || 0;
@@ -1568,13 +1602,13 @@ async function startMailRuDownload(url) {
}
startMailRuProgressPolling(job);
launchMailRuJob(job);
return true;
} catch (err) {
job.state = "error";
job.error = err?.message || "Mail.ru indirimi başlatılamadı";
scheduleSnapshotBroadcast();
return false;
}
return job;
}
function findYoutubeInfoJson(savePath) {
@@ -1737,10 +1771,18 @@ function removeMailRuJob(jobId, { removeFiles = true } = {}) {
}
mailruJobs.delete(jobId);
let filesRemoved = false;
if (removeFiles && job.savePath && fs.existsSync(job.savePath)) {
if (removeFiles) {
try {
fs.rmSync(job.savePath, { recursive: true, force: true });
filesRemoved = true;
if (job.savePath === DOWNLOAD_DIR && job.fileName) {
const filePath = path.join(job.savePath, job.fileName);
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { force: true });
filesRemoved = true;
}
} else if (job.savePath && fs.existsSync(job.savePath)) {
fs.rmSync(job.savePath, { recursive: true, force: true });
filesRemoved = true;
}
} catch (err) {
console.warn("Mail.ru dosyası silinemedi:", err.message);
}
@@ -5746,7 +5788,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
const isDirectory = stats.isDirectory();
const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/");
let trashEntry = null;
if (folderId && rootDir) {
if (folderId && rootDir && fs.existsSync(rootDir) && fs.statSync(rootDir).isDirectory()) {
const infoBeforeDelete = readInfoForRoot(folderId);
mediaFlags = detectMediaFlagsForPath(
infoBeforeDelete,
@@ -5757,7 +5799,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
mediaFlags = { movies: false, tv: false };
}
if (folderId && rootDir) {
if (folderId && rootDir && fs.existsSync(rootDir) && fs.statSync(rootDir).isDirectory()) {
trashEntry = addTrashEntry(folderId, {
path: relWithinRoot,
originalPath: safePath,
@@ -6737,6 +6779,53 @@ app.post("/api/mailru/download", requireAuth, async (req, res) => {
}
});
app.post("/api/mailru/match", requireAuth, async (req, res) => {
try {
const { jobId, metadata, season, episode } = req.body || {};
if (!jobId || !metadata) {
return res.status(400).json({ ok: false, error: "jobId ve metadata gerekli." });
}
const job = mailruJobs.get(jobId);
if (!job) {
return res.status(404).json({ ok: false, error: "Mail.ru işi bulunamadı." });
}
if (job.state !== "awaiting_match") {
if (job.match && job.fileName) {
return res.json({
ok: true,
jobId: job.id,
fileName: job.fileName,
alreadyMatched: true
});
}
return res.status(400).json({ ok: false, error: "Mail.ru işi eşleştirme beklemiyor." });
}
const safeSeason = Number(season) || 1;
const safeEpisode = Number(episode) || 1;
const title = metadata.title || metadata.name || "Anime";
job.match = {
id: metadata.id || null,
title,
season: safeSeason,
episode: safeEpisode,
matchedAt: Date.now()
};
job.fileName = formatMailRuSeriesFilename(title, safeSeason, safeEpisode);
job.title = job.fileName;
const started = await beginMailRuDownload(job);
if (!started) {
return res.status(500).json({ ok: false, error: job.error || "Mail.ru indirimi başlatılamadı." });
}
console.log(`✅ Mail.ru eşleştirme tamamlandı: ${job.fileName}`);
res.json({ ok: true, jobId: job.id, fileName: job.fileName });
} catch (err) {
res.status(500).json({
ok: false,
error: err?.message || "Mail.ru eşleştirme başarısız oldu."
});
}
});
// --- 🎫 YouTube cookies yönetimi ---
app.get("/api/youtube/cookies", requireAuth, (req, res) => {
try {