diff --git a/archive.ts b/archive.ts index 7d87b55..7e1ff51 100644 --- a/archive.ts +++ b/archive.ts @@ -1,10 +1,10 @@ -export async function archive(src_folder: string, archive_name: string): Promise { - const cmd: string[] = ["7z", "a", archive_name]; +import { normalize, posix } from "https://deno.land/std@0.174.0/path/mod.ts"; - for (const file of Deno.readDirSync(src_folder)) { - if (file.isSymlink) continue; - cmd.push(`${src_folder}/${file.name}`); - } +export async function archive(src_folder: string, archive_name: string, delete_src = true): Promise { + try { + Deno.removeSync(archive_name, { recursive: true }); + } catch (_e) {} + const cmd: string[] = ["7z", "a", archive_name, posix.normalize(`${posix.normalize(normalize(`${src_folder}`))}\\*`)]; const process = Deno.run({ cmd, @@ -12,5 +12,7 @@ export async function archive(src_folder: string, archive_name: string): Promise stderr: "piped", }); - return (await process.status()).success; + if (!(await process.status()).success) return false; + if (delete_src) Deno.removeSync(src_folder, { recursive: true }); + return true; } diff --git a/build_linux.sh b/build_linux.sh index 3956509..0f9fb01 100644 --- a/build_linux.sh +++ b/build_linux.sh @@ -1,2 +1,2 @@ # Linux -deno compile --allow-net --allow-read --allow-write --allow-env --target x86_64-unknown-linux-gnu --output mangasee-dl main.ts +deno compile --allow-net --allow-read --allow-write --allow-env --allow-run --target x86_64-unknown-linux-gnu --output mangasee-dl main.ts diff --git a/build_windows.sh b/build_windows.sh index 8eba346..7361ab8 100644 --- a/build_windows.sh +++ b/build_windows.sh @@ -1,2 +1,2 @@ # Windows -deno compile --allow-net --allow-read --allow-write --allow-env --target x86_64-pc-windows-msvc --output mangasee-dl.exe main.ts +deno compile --allow-net --allow-read --allow-write --allow-env --allow-run --target x86_64-pc-windows-msvc --output mangasee-dl.exe main.ts diff --git a/comic-info.ts b/comic-info.ts index caf6127..9f676e3 100644 --- a/comic-info.ts +++ b/comic-info.ts @@ -3,8 +3,7 @@ import { BasicMangaMetadata, Chapter } from "./metadata.ts"; export interface PageInfo { index: number; - width: number; - height: number; + size: number; filename: string; } @@ -30,10 +29,11 @@ export function writeComicInfo(folder_path: string, metadata: BasicMangaMetadata } ); Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t${metadata.title}\n`), { create: false, append: true }); - Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t${chapter_info.main}\n`), { create: false, append: true }); + Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t${chapter_info.main}${chapter_info.sub != 0 ? `.${chapter_info.sub}` : ""}\n`), { + create: false, + append: true, + }); Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t${chapter_info.pretype}\n`), { create: false, append: true }); - if (chapter_info.sub != 0) - Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t${chapter_info.sub}\n`), { create: false, append: true }); Deno.writeFileSync(FILE_PATH, Encoder.encode(`\tDownloaded/Scrapped with Mangasee123 Downloader from MrMysterius\n`), { create: false, append: true, @@ -47,7 +47,7 @@ export function writeComicInfo(folder_path: string, metadata: BasicMangaMetadata Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t\n`), { create: false, append: true }); for (const page of pages) { - Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t\t\n`), { + Deno.writeFileSync(FILE_PATH, Encoder.encode(`\t\t\n`), { create: false, append: true, }); diff --git a/download.ts b/download.ts new file mode 100644 index 0000000..8b1d7f4 --- /dev/null +++ b/download.ts @@ -0,0 +1,154 @@ +import { BasicMangaMetadata, Chapter } from "./metadata.ts"; +import { PageInfo } from "./comic-info.ts"; +import { join } from "https://deno.land/std@0.174.0/path/mod.ts"; +import { saveProgress } from "./progress.ts"; +import { writeComicInfo } from "./comic-info.ts"; +import { archive } from "./archive.ts"; +import * as Color from "https://deno.land/std@0.174.0/fmt/colors.ts"; +import { zeroSpaceOut } from "./main.ts"; + +const BASE_CHAPTER_URL = "https://mangasee123.com/read-online/"; + +export async function getChapterPage(mangaIndexName: string, chapter: Chapter) { + if (chapter.pretype == 1) { + const res = await fetch(`${BASE_CHAPTER_URL}${mangaIndexName}-chapter-${chapter.main}${chapter.sub != 0 ? `.${chapter.sub}` : ""}.html`); + return await res.text(); + } else { + const res = await fetch( + `${BASE_CHAPTER_URL}${mangaIndexName}-chapter-${chapter.main}${chapter.sub != 0 ? `.${chapter.sub}` : ""}-index-${chapter.pretype}.html` + ); + return await res.text(); + } +} + +interface ChapterInfo { + Chapter: string; + Page: string; + Directory: string; +} + +export function extractChapterInfo(html_document: string): ChapterInfo | null { + const match = html_document.match(/vm\.CurChapter = (.+);/m); + return JSON.parse(match?.[1] || "[]") || null; +} + +export function extractHost(html_document: string) { + const match = html_document.match(/vm\.CurPathName = "(.+)";/m); + return match?.[1] || null; +} + +export async function downloadChapter(mangaIndexName: string, chapter: Chapter, folder_path: string): Promise { + const html_document = await getChapterPage(mangaIndexName, chapter); + const chapter_info = extractChapterInfo(html_document); + const host = extractHost(html_document); + + if (chapter_info == null || host == null) return null; + + let chapter_number = ""; + const main_number_string = chapter.main.toString(); + if (main_number_string.length <= 4) { + chapter_number = "0".repeat(4 - main_number_string.length) + main_number_string; + } else { + chapter_number = main_number_string; + } + if (chapter.sub != 0) { + chapter_number += `.${chapter.sub}`; + } + + const DIRECTORY_STRING = chapter_info.Directory != "" ? `${chapter_info.Directory}/` : ""; + const CHAPTER_PREFIX = `${chapter_number}-`; + const BASE_DOWNLOAD_URL = `https://${host}/manga/${mangaIndexName}/${DIRECTORY_STRING}${CHAPTER_PREFIX}`; + + const pages: PageInfo[] = []; + + const page_count = parseInt(chapter_info.Page); + for (let i = 1; i <= page_count; i++) { + let page_number = ""; + const i_string = i.toString(); + if (i_string.length <= 3) { + page_number = "0".repeat(3 - i_string.length) + i_string; + } else { + page_number = i_string; + } + + const res = await fetch(`${BASE_DOWNLOAD_URL}${page_number}.png`); + const file = await Deno.open(join(folder_path, `${i}.png`), { create: true, write: true }); + + await res.body?.pipeTo(file.writable); + try { + file.close(); + } catch (_e) {} + const size = parseInt(res.headers.get("content-length") as string); + + pages.push({ index: i - 1, filename: `${i}.png`, size: size }); + } + + return pages; +} + +export async function download( + mangaIndexName: string, + metadata: BasicMangaMetadata, + folder_path: string, + chapters: Chapter[], + start: Chapter, + end: Chapter, + current: Chapter | null = null +) { + const BASE_FOLDER_PATH = join(folder_path, mangaIndexName); + Deno.mkdirSync(BASE_FOLDER_PATH, { recursive: true }); + + if (current == null) current = start; + if (!findChapter(chapters, current)?.chapter) return false; + + chapters = chaptersSort(chapters); + let init = true; + + for (let chapter of chapters) { + if (chapter.raw != current.raw && init) { + continue; + } + init = false; + + Deno.stdout.write( + new TextEncoder().encode(`${Color.green("Downloading: ")} ${Color.blue(`v${chapter.pretype} - c${zeroSpaceOut(chapter.main, 4)}.${chapter.sub}`)}`) + ); + + current = chapter; + saveProgress(BASE_FOLDER_PATH, { current: current, start: start, end: end }); + + const CHAPTER_PATH = join(BASE_FOLDER_PATH, `${mangaIndexName}-v${current.pretype}-c${current.main}${current.sub != 0 ? `.${current.sub}` : ""}`); + try { + Deno.removeSync(CHAPTER_PATH, { recursive: true }); + } catch (_e) {} + Deno.mkdirSync(CHAPTER_PATH, { recursive: true }); + const pages = await downloadChapter(mangaIndexName, current, CHAPTER_PATH); + if (pages == null) return false; + + Deno.stdout.write(new TextEncoder().encode(`${Color.white(" Done |")} ${Color.yellow("Writing Metadata")}`)); + if (!writeComicInfo(CHAPTER_PATH, metadata, current, pages)) return false; + Deno.stdout.write(new TextEncoder().encode(`${Color.white(" Done |")} ${Color.magenta("Archiving")}`)); + if (!(await archive(CHAPTER_PATH, `${CHAPTER_PATH}.cb7`))) return false; + Deno.stdout.write(new TextEncoder().encode(`${Color.white(" Done |\n")}`)); + + if (chapter.raw == end.raw) break; + } + + return true; +} + +export interface PartChapter { + pretype: number; + main: number; + sub: number; +} + +export function findChapter(chapters: Chapter[], search: PartChapter) { + const index = chapters.findIndex((chapter) => chapter.pretype == search.pretype && chapter.main == search.main && chapter.sub == search.sub); + if (index == -1) return null; + return { chapter: chapters[index], index }; +} + +export function chaptersSort(chapters: Chapter[]) { + return chapters.sort((a, b) => parseInt(a.raw) - parseInt(b.raw)); +} diff --git a/main.ts b/main.ts index 38f806f..d0ef5f9 100644 --- a/main.ts +++ b/main.ts @@ -1,18 +1,25 @@ import { parse } from "https://deno.land/std@0.174.0/flags/mod.ts"; import * as Color from "https://deno.land/std@0.174.0/fmt/colors.ts"; import { searchAnime } from "./search.ts"; -import { getBasicMetadata } from "./metadata.ts"; +import { Chapter, getBasicMetadata, getFullMetadata } from "./metadata.ts"; +import { download, chaptersSort, findChapter } from "./download.ts"; +import { loadProgress } from "./progress.ts"; +import { join } from "https://deno.land/std@0.174.0/path/mod.ts"; const ARGS = parse(Deno.args); switch (ARGS._[0] || undefined) { case "search": { if (ARGS._.length <= 1) break; + const results = await searchAnime(ARGS._.slice(1, ARGS._.length).join(" "), parseInt(ARGS.l) || parseInt(ARGS["limit"]) || 5); + for (const result of results) { if (result.score > (parseInt(ARGS.s) || parseInt(ARGS["score-threshold"]) || 0.05)) continue; + let release_year = ""; let chapters_available = ""; + if (!ARGS["no-metadata"] && !ARGS.n) { const metadata = await getBasicMetadata(result.item.i); release_year = metadata?.release_year || ""; @@ -26,7 +33,102 @@ switch (ARGS._[0] || undefined) { } break; } + case "download": { + if (!ARGS._[1]) break; + + const metadata = await getFullMetadata(ARGS._[1] as string); + if (metadata == null) break; + metadata.chapters = chaptersSort(metadata.chapters); + + const progress = loadProgress(join(Deno.cwd(), ARGS._[1] as string)); + const start_part = ARGS.s?.toString().split(".") || ARGS["start"]?.toString().split(".") || null; + const end_part = ARGS.e?.toString().split(".") || ARGS["end"]?.toString().split(".") || null; + + let start: Chapter = { pretype: 0, main: 0, sub: 0, raw: "" }; + let end: Chapter = { pretype: 0, main: 0, sub: 0, raw: "" }; + let current: Chapter = { pretype: 0, main: 0, sub: 0, raw: "" }; + + if (progress != undefined) { + start = progress.start; + end = progress.end; + current = progress.current; + } + + if (start_part != null) { + let c: Chapter = start; + switch (start_part.length) { + case 3: + c = { pretype: start_part[0], main: start_part[1], sub: start_part[2], raw: `${start_part[0]}${zeroSpaceOut(start_part[1], 4)}${start_part[2]}` }; + break; + case 2: + c = { pretype: 1, main: start_part[0], sub: start_part[1], raw: `1${zeroSpaceOut(start_part[0], 4)}${start_part[1]}` }; + break; + case 1: + c = { pretype: 1, main: start_part[0], sub: 0, raw: `1${zeroSpaceOut(start_part[0], 4)}0` }; + break; + } + if (start.raw != c.raw) { + start = c; + current = start; + } + } else if (progress == undefined) { + start = metadata.chapters[0]; + current = metadata.chapters[0]; + } + + if (end_part != null) { + let c = end; + switch (end_part.length) { + case 3: + c = { pretype: end_part[0], main: end_part[1], sub: end_part[2], raw: `${end_part[0]}${zeroSpaceOut(end_part[1], 4)}${end_part[2]}` }; + break; + case 2: + c = { pretype: 1, main: end_part[0], sub: end_part[1], raw: `1${zeroSpaceOut(end_part[0], 4)}${end_part[1]}` }; + break; + case 1: + c = { pretype: 1, main: end_part[0], sub: 0, raw: `1${zeroSpaceOut(end_part[0], 4)}0` }; + break; + } + if (end.raw != c.raw) { + const find_end = findChapter(metadata.chapters, end); + const find_c = findChapter(metadata.chapters, c); + if (find_end == null) { + end = c; + } else if (find_c != null && find_end.index <= find_c.index) { + end = c; + } else if (find_c != null && find_end.index > find_c.index) { + end = c; + current = start; + } + } + } else if (progress == undefined) { + end = metadata.chapters[metadata.chapters.length - 1]; + } + + console.log(Color.green("# Starting Download #")); + console.log(Color.magenta(`Manga: ${metadata.basic.title}`)); + console.log(Color.yellow(`Author(s): ${metadata.basic.authors.join(", ")}`)); + console.log(Color.cyan(`Genre(s): ${metadata.basic.genres.join(", ")}`)); + + console.log(Color.bold("\nFrom:\t\t"), Color.blue(`v${start.pretype} - c${zeroSpaceOut(start.main, 4)}.${start.sub}`)); + console.log(Color.bold("To:\t\t"), Color.brightYellow(`v${end.pretype} - c${zeroSpaceOut(end.main, 4)}.${end.sub}`)); + console.log(Color.bold("Continuing:\t"), Color.brightRed(`v${current.pretype} - c${zeroSpaceOut(current.main, 4)}.${current.sub}`)); + + console.log(Color.gray("\n----------------------------------------")); + + await download(ARGS._[1] as string, metadata.basic, Deno.cwd(), metadata.chapters, start, end, current); + + console.log(Color.green("# Finished Download #")); + + break; + } default: console.log(Color.red("# No Command Supplied - Aborting #")); break; } + +export function zeroSpaceOut(number: string | number, spaces: number) { + const number_string = number.toString(); + if (number_string.length >= spaces) return number_string; + return "0".repeat(spaces - number_string.length) + number_string; +} diff --git a/metadata.ts b/metadata.ts index 31dac5b..ff3b167 100644 --- a/metadata.ts +++ b/metadata.ts @@ -49,12 +49,14 @@ export function extractMetadata(html_document: string) { switch (label?.innerText) { case "Author(s):": { child.querySelectorAll("a").forEach((a) => { + //@ts-ignore property innerText does exist but is not in the Interface/Declaration metadata.authors.push(a.innerText); }); continue; } case "Genre(s):": { child.querySelectorAll("a").forEach((a) => { + //@ts-ignore property innerText does exist but is not in the Interface/Declaration metadata.genres.push(a.innerText); }); continue; @@ -87,6 +89,7 @@ export interface Chapter { pretype: number; main: number; sub: number; + raw: string; } export async function getChapters(mangaIndexName: string) { @@ -107,6 +110,7 @@ export function extractChapters(html_document: string) { pretype: parseInt(match[1]), main: parseInt(match[2]), sub: parseInt(match[3]), + raw: chapter.Chapter, }); } diff --git a/progress.ts b/progress.ts index f0716dd..ca97fa8 100644 --- a/progress.ts +++ b/progress.ts @@ -1,9 +1,10 @@ import { join } from "https://deno.land/std@0.174.0/path/mod.ts"; +import { Chapter } from "./metadata.ts"; interface Progress { - current: number; - start: number; - end: number; + current: Chapter; + start: Chapter; + end: Chapter; } export function saveProgress(target_folder: string, progress: Progress): boolean { @@ -26,8 +27,7 @@ export function loadProgress(target_folder: string) { try { const progress: Progress = JSON.parse(Decoder.decode(Deno.readFileSync(FILE_PATH))); return progress; - } catch (err) { - console.error(err); + } catch (_e) { return undefined; } }