diff --git a/setup.vitest.ts b/setup.vitest.ts new file mode 100644 index 00000000..33a6022e --- /dev/null +++ b/setup.vitest.ts @@ -0,0 +1,26 @@ +window.Http = { + version: "0.0.21", + get(options) { + return fetch(options.url) + .then((res) => res.text()) + .then((text) => { + return { + data: text, + status: 200, + } + }) + }, + post(options) { + return fetch(options.url, { + method: "POST", + body: JSON.stringify(options.data), + }) + .then((res) => res.text()) + .then((text) => { + return { + data: text, + status: 200, + } + }) + }, +} diff --git a/src/apis/parser/myanimelist/episodes.ts b/src/apis/parser/myanimelist/episodes.ts new file mode 100644 index 00000000..f6404b8f --- /dev/null +++ b/src/apis/parser/myanimelist/episodes.ts @@ -0,0 +1,18 @@ +import { parserDom } from "../__helpers__/parserDom" + +export default function AnimeEpisodes(html: string) { + const $ = parserDom(html) + + return $(".episode-list-data") + .toArray() + .map((ep) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const number = $(ep).find(".episode-number").attr("data-raw")! + const name = $(ep).find(".episode-title > a").text().trim() + const japanese = $(ep).find(".di-ib").text().trim() + const time = $(ep).find(".episode-aired").text().trim() + const average = $(ep).find(".episode-aired").text() + + return { number, name, japanese, time, average } + }) +} diff --git a/src/apis/parser/myanimelist/search.ts b/src/apis/parser/myanimelist/search.ts new file mode 100644 index 00000000..5e1bad69 --- /dev/null +++ b/src/apis/parser/myanimelist/search.ts @@ -0,0 +1,37 @@ +import { parserDom } from "../__helpers__/parserDom" + +interface AnimeItem { + name_lower: string + search_name: string + id: number + type: string + name: string + url: string + image_url: string +} + +export default function AnimeSearch(html: string) { + const $ = parserDom(html) + + return $(".js-categories-seasonal tr") + .toArray() + .map((tr) => { + const url = $(tr).find(".hoverinfo_trigger").attr("href") + if (!url) return null + + const indexParamAnime = url.indexOf("/anime/") + 7 + + if (indexParamAnime === -1) return null + + const id = parseInt( + url.slice(indexParamAnime, url.indexOf("/", indexParamAnime)) + ) + const name = $(tr).find("strong").text() + // eslint-disable-next-line camelcase + const image_url = $(tr).find("img").attr("src") + + // eslint-disable-next-line camelcase + return { id, type: "anime", name, url, image_url } + }) + .filter(Boolean) as AnimeItem[] +} diff --git a/src/apis/runs/myanimelist/episodes.spec.ts b/src/apis/runs/myanimelist/episodes.spec.ts new file mode 100644 index 00000000..546ed479 --- /dev/null +++ b/src/apis/runs/myanimelist/episodes.spec.ts @@ -0,0 +1,106 @@ +import { getEpisodesMyAnimeList } from "./episodes" + +describe("episodes", () => { + describe("getEpisodesMyAnimeList", () => { + test("should work in all of one page", async () => { + expect( + await getEpisodesMyAnimeList( + "https://myanimelist.net/anime/41389/Tonikaku_Kawaii" + ).then((items) => + items.map((item) => { + delete item.japanese + return item + }) + ) + ).toEqual([ + { + number: "1", + name: "Marriage", + time: "Oct 3, 2020", + average: "Oct 3, 2020", + }, + { + number: "2", + name: "The First Night", + time: "Oct 10, 2020", + average: "Oct 10, 2020", + }, + { + number: "3", + name: "Sisters", + time: "Oct 17, 2020", + average: "Oct 17, 2020", + }, + { + number: "4", + name: "Promise", + time: "Oct 24, 2020", + average: "Oct 24, 2020", + }, + { + number: "5", + name: "Rings", + time: "Oct 31, 2020", + average: "Oct 31, 2020", + }, + { + number: "6", + name: "News", + time: "Nov 7, 2020", + average: "Nov 7, 2020", + }, + { + number: "7", + name: "Trip", + time: "Nov 14, 2020", + average: "Nov 14, 2020", + }, + { + number: "8", + name: "Parents", + time: "Nov 21, 2020", + average: "Nov 21, 2020", + }, + { + number: "9", + name: "Daily Life", + time: "Nov 28, 2020", + average: "Nov 28, 2020", + }, + { + number: "10", + name: "The Way Home", + time: "Dec 5, 2020", + average: "Dec 5, 2020", + }, + { + number: "11", + name: "Friends", + time: "Dec 12, 2020", + average: "Dec 12, 2020", + }, + { + number: "12", + name: "Husband and Wife", + time: "Dec 19, 2020", + average: "Dec 19, 2020", + }, + { + number: "13", + name: "SNS", + time: "N/A", + average: "N/A", + }, + ]) + }) + + test("should search by offset", async () => { + const diff = await getEpisodesMyAnimeList( + "https://myanimelist.net/anime/235/Detective_Conan", + 100 + ) + + expect(diff.length).toBe(100) + }) + }) +}) diff --git a/src/apis/runs/myanimelist/episodes.ts b/src/apis/runs/myanimelist/episodes.ts new file mode 100644 index 00000000..c2b92fce --- /dev/null +++ b/src/apis/runs/myanimelist/episodes.ts @@ -0,0 +1,22 @@ +// first in query id anime + +import type MyAnimeListEpisodesParser from "src/apis/parser/myanimelist/episodes" +import { get } from "src/logic/http" + +import { useCache } from "../../useCache" +import Worker from "../../workers/myanimelist/episodes?worker" +import { PostWorker } from "../../wrap-worker" + +export async function getEpisodesMyAnimeList(url: string, offset: number = 0) { + return await useCache(`${url}/episode`, async () => { + const html = await (await get(`${url}/episode?offset=${offset}`)).data + + if (import.meta.env.MODE === "test") { + return import("../../parser/myanimelist/episodes").then((res) => + res.default(html) + ) + } + + return PostWorker(Worker, html) + }) +} diff --git a/src/apis/runs/myanimelist/search.spec.ts b/src/apis/runs/myanimelist/search.spec.ts new file mode 100644 index 00000000..a8ee9da8 --- /dev/null +++ b/src/apis/runs/myanimelist/search.spec.ts @@ -0,0 +1,434 @@ +import { getAmimeMyAnimeList } from "./search" + +describe("episodes", () => { + describe("getAmimeMyAnimeList", () => { + test("Gate: Jieitai Kanochi nite, Kaku Tatakaeri", async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const anime = (await getAmimeMyAnimeList( + "Cổng chiến tranh", + "GATE, Gate: Thus the JSDF Fought There!, Gate: Jieitai Kanochi nite, Kaku Tatakaeri" + ))! + + expect(anime.id).toBe(28907) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/28907/Gate__Jieitai_Kanochi_nite_Kaku_Tatakaeri" + ) + expect(anime.name).toBe("Gate: Jieitai Kanochi nite, Kaku Tatakaeri") + }) + + test("Gate: Jieitai Kanochi nite, Kaku Tatakaeri 2nd Season", async () => { + const anime = await getAmimeMyAnimeList( + "Cổng chiến tranh SS2", + "GATE, Gate: Thus the JSDF Fought There! Fire Dragon Arc, Gate: Jieitai Kanochi nite, Kaku Tatakaeri - Enryuu-hen, Gate: Jieitai Kanochi nite, Kaku Tatakaeri 2nd Season" + ) + + expect(anime.id).toBe(31637) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/31637/Gate__Jieitai_Kanochi_nite_Kaku_Tatakaeri_Part_2" + ) + expect(anime.name).toBe( + "Gate: Jieitai Kanochi nite, Kaku Tatakaeri Part 2" + ) + }) + + test("Tonikaku Kawaii", async () => { + const anime = await getAmimeMyAnimeList( + "Tonikaku Kawaii", + "TONIKAWA: Over the Moon For You:, Generally Cute, Fly Me to the Moon" + ) + + expect(anime.id).toBe(41389) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/41389/Tonikaku_Kawaii" + ) + expect(anime.name).toBe("Tonikaku Kawaii") + }) + + test("Tonikaku Kawaii: SNS", async () => { + const anime = await getAmimeMyAnimeList( + "Tonikaku Kawaii: SNS", + "Tonikaku Kawaii OVA, Tonikawa" + ) + + expect(anime.id).toBe(44931) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/44931/Tonikaku_Kawaii__SNS" + ) + expect(anime.name).toBe("Tonikaku Kawaii: SNS") + }) + + test("Tonikaku Kawaii: Seifuku", async () => { + const anime = await getAmimeMyAnimeList( + "Tonikaku Kawaii: Seifuku", + "Tonikawa: Over the Moon for You - Uniform" + ) + + expect(anime.id).toBe(51533) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/51533/Tonikaku_Kawaii__Seifuku" + ) + expect(anime.name).toBe("Tonikaku Kawaii: Seifuku") + }) + + test("Tonikaku Kawaii 2nd Season", async () => { + const anime = await getAmimeMyAnimeList( + "Tonikaku Kawaii 2nd Season", + "Tonikawa: Over the Moon for You 2nd Season, Tonikawa: Over The Moon For You Season 2" + ) + + expect(anime.id).toBe(50307) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/50307/Tonikaku_Kawaii_2nd_Season" + ) + expect(anime.name).toBe("Tonikaku Kawaii 2nd Season") + }) + + test("86", async () => { + const anime = await getAmimeMyAnimeList("86", "Eighty Six") + + expect(anime.id).toBe(41457) + expect(anime.type).toBe("anime") + expect(anime.url).toBe("https://myanimelist.net/anime/41457/86") + expect(anime.name).toBe("86") + }) + + test("86 Special Edition: Senya ni Akaku Hinageshi no Saku", async () => { + const anime = await getAmimeMyAnimeList( + "86 Special Edition: Senya ni Akaku Hinageshi no Saku", + "86-Eighty Six- Special Edition - Coquelicots Blooming Across the Battlefield, Eighty Six Special Edition" + ) + + expect(anime.id).toBe(49235) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/49235/86_Special_Edition__Senya_ni_Akaku_Hinageshi_no_Saku" + ) + expect(anime.name).toBe( + "86 Special Edition: Senya ni Akaku Hinageshi no Saku" + ) + }) + + test("86 2nd Season", async () => { + const anime = await getAmimeMyAnimeList( + "86 2nd Season", + "86 Eighty-Six, Eighty Six 2nd Season" + ) + + expect(anime.id).toBe(48569) + expect(anime.type).toBe("anime") + expect(anime.url).toBe("https://myanimelist.net/anime/48569/86_Part_2") + expect(anime.name).toBe("86 Part 2") + }) + + test("Mahoutsukai no Yome: Hoshi Matsu Hito", async () => { + const anime = await getAmimeMyAnimeList( + "Mahoutsukai no Yome: Hoshi Matsu Hito", + "The Ancient Magus' Bride: Those Awaiting a Star, The Magician's Bride" + ) + + expect(anime.id).toBe(32902) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/32902/Mahoutsukai_no_Yome__Hoshi_Matsu_Hito" + ) + expect(anime.name).toBe("Mahoutsukai no Yome: Hoshi Matsu Hito") + }) + + test("Mahoutsukai no Yome", async () => { + const anime = await getAmimeMyAnimeList( + "Mahoutsukai no Yome", + "The Ancient Magus' Bride, The Magician's Bride, Cô Dâu Pháp Sư" + ) + + expect(anime.id).toBe(35062) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/35062/Mahoutsukai_no_Yome" + ) + expect(anime.name).toBe("Mahoutsukai no Yome") + }) + + test("Mahoutsukai no Yome: Nishi no Shounen to Seiran no Kishi", async () => { + const anime = await getAmimeMyAnimeList( + "Mahoutsukai no Yome: Nishi no Shounen to Seiran no Kishi", + "The Ancient Magus' Bride: The Boy from the West and the Knight of the Blue Storm, The Ancient Magus' Bride OVA, Mahoutsukai no Yome OVA, Mahoutsuaki no Yome OAD" + ) + + expect(anime.id).toBe(48438) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/48438/Mahoutsukai_no_Yome__Nishi_no_Shounen_to_Seiran_no_Kishi" + ) + expect(anime.name).toBe( + "Mahoutsukai no Yome: Nishi no Shounen to Seiran no Kishi" + ) + }) + + test("Mahoutsukai no Yome Season 2", async () => { + const anime = await getAmimeMyAnimeList( + "Cô Dâu Pháp Sư Mùa 2", + "Mahoutsukai no Yome Season 2, The Ancient Magus' Bride Season 2, The Ancient Magus Bride 2, Mahoutsukai no Yome 2, Mahoyome" + ) + + expect(anime.id).toBe(52955) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/52955/Mahoutsukai_no_Yome_Season_2" + ) + expect(anime.name).toBe("Mahoutsukai no Yome Season 2") + }) + + test("Anh Thợ Saitou Đa Năng Ở Dị Giới", async () => { + const anime = await getAmimeMyAnimeList( + "Anh Thợ Saitou Đa Năng Ở Dị Giới", + "Benriya Saitou-san, Isekai ni Iku, Handyman Saitou in Another Worlde" + ) + + expect(anime.id).toBe(50854) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/50854/Benriya_Saitou-san_Isekai_ni_Iku" + ) + expect(anime.name).toBe("Benriya Saitou-san, Isekai ni Iku") + }) + + test("Naruto", async () => { + const anime = await getAmimeMyAnimeList("Naruto", "Naruto, NARUTO") + + expect(anime.id).toBe(20) + expect(anime.type).toBe("anime") + expect(anime.url).toBe("https://myanimelist.net/anime/20/Naruto") + expect(anime.name).toBe("Naruto") + }) + + test("Naruto: Sức Mạnh Vĩ Thú", async () => { + const anime = await getAmimeMyAnimeList( + "Naruto: Sức Mạnh Vĩ Thú", + "Naruto Shippuden, Naruto Hurricane Chronicles, Naruto: Shippuuden" + ) + + expect(anime.id).toBe(1735) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/1735/Naruto__Shippuuden" + ) + expect(anime.name).toBe("Naruto: Shippuuden") + }) + + test("Thám Tử Lừng Danh Conan", async () => { + const anime = await getAmimeMyAnimeList( + "Thám Tử Lừng Danh Conan", + "Detective Conan, Case Closed, Meitantei Conan" + ) + + expect(anime.id).toBe(235) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/235/Detective_Conan" + ) + expect(anime.name).toBe("Detective Conan") + }) + + test("Detective Conan Movie 26: Kurogane no Submarine", async () => { + const anime = await getAmimeMyAnimeList( + "Detective Conan Movie 26: Kurogane no Submarine", + "Meitantei Conan: Kurogane no Submarine" + ) + + expect(anime.id).toBe(53540) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/53540/Detective_Conan_Movie_26__Kurogane_no_Submarine" + ) + expect(anime.name).toBe("Detective Conan Movie 26: Kurogane no Submarine") + }) + + test("Mở Ra Một Thế Giới Tuyệt Vời", async () => { + const anime = await getAmimeMyAnimeList( + "Mở Ra Một Thế Giới Tuyệt Vời", + "KonoSuba: God's Blessing on This Wonderful World!, Give Blessings to This Wonderful World!,Kono Subarashii Sekai ni Shukufuku wo!" + ) + + expect(anime.id).toBe(30831) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/30831/Kono_Subarashii_Sekai_ni_Shukufuku_wo" + ) + expect(anime.name).toBe("Kono Subarashii Sekai ni Shukufuku wo!") + }) + + test("Mở Ra Một Thế Giới Tuyệt Vời OVA", async () => { + const anime = await getAmimeMyAnimeList( + "Mở Ra Một Thế Giới Tuyệt Vời OVA", + "KonoSuba OVA, A Blessing to this Wonderful Choker!, Kono Subarashii Choker ni Shufuku wo!, Kono Subarashii Sekai ni Shukufuku wo! OVA" + ) + + expect(anime.id).toBe(32380) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/32380/Kono_Subarashii_Sekai_ni_Shukufuku_wo_Kono_Subarashii_Choker_ni_Shukufuku_wo" + ) + expect(anime.name).toBe( + "Kono Subarashii Sekai ni Shukufuku wo! Kono Subarashii Choker ni Shukufuku wo!" + ) + }) + + test("Mở Ra Một Thế Giới Tuyệt Vời 2", async () => { + const anime = await getAmimeMyAnimeList( + "Mở Ra Một Thế Giới Tuyệt Vời 2", + "KonoSuba: God's Blessing on This Wonderful World! 2, Give Blessings to This Wonderful World! 2, Kono Subarashii Sekai ni Shukufuku wo! 2" + ) + + expect(anime.id).toBe(32937) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/32937/Kono_Subarashii_Sekai_ni_Shukufuku_wo_2" + ) + expect(anime.name).toBe("Kono Subarashii Sekai ni Shukufuku wo! 2") + }) + + test("Mở Ra Một Thế Giới Tuyệt Vời OVA 2", async () => { + const anime = await getAmimeMyAnimeList( + "Mở Ra Một Thế Giới Tuyệt Vời OVA 2", + "KonoSuba: God's Blessing on This Wonderful World! Second Season OVA, KonoSuba: God's Blessing on This Wonderful World! Second Season OVA, Kono Subarashii Sekai ni Shukufuku wo! 2 OVA" + ) + + expect(anime.id).toBe(34626) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/34626/Kono_Subarashii_Sekai_ni_Shukufuku_wo_2__Kono_Subarashii_Geijutsu_ni_Shukufuku_wo" + ) + expect(anime.name).toBe( + "Kono Subarashii Sekai ni Shukufuku wo! 2: Kono Subarashii Geijutsu ni Shukufuku wo!" + ) + }) + + test("Kono Subarashii Sekai ni Bakuen wo!", async () => { + const anime = await getAmimeMyAnimeList( + "Kono Subarashii Sekai ni Bakuen wo!", + "Konosuba: An Explosion on This Wonderful World!" + ) + + expect(anime.id).toBe(51958) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/51958/Kono_Subarashii_Sekai_ni_Bakuen_wo" + ) + expect(anime.name).toBe("Kono Subarashii Sekai ni Bakuen wo!") + }) + + test("Onegai☆Teacher", async () => { + const anime = await getAmimeMyAnimeList( + "Onegai☆Teacher", + "Please Teacher!, Onegai Sensei" + ) + + expect(anime.id).toBe(195) + expect(anime.type).toBe("anime") + expect(anime.url).toBe("https://myanimelist.net/anime/195/Onegai☆Teacher") + expect(anime.name).toBe("Onegai☆Teacher") + }) + + test("Karakai Jouzu no Takagi-san", async () => { + const anime = await getAmimeMyAnimeList( + "Karakai Jouzu no Takagi-san", + "Skilled Teaser Takagi-san" + ) + + expect(anime.id).toBe(35860) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/35860/Karakai_Jouzu_no_Takagi-san" + ) + expect(anime.name).toBe("Karakai Jouzu no Takagi-san") + }) + + test("Nhất Quỷ Nhì Ma, Thứ Ba Takagi Phần 3", async () => { + const anime = await getAmimeMyAnimeList( + "Nhất Quỷ Nhì Ma, Thứ Ba Takagi Phần 3", + "Karakai Jouzu no Takagi-san 3, Skilled Teaser Takagi-san 3rd Season, Karakai Jouzu no Takagi-san Third Season, Teasing Master Takagi-san" + ) + + expect(anime.id).toBe(49721) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/49721/Karakai_Jouzu_no_Takagi-san_3" + ) + expect(anime.name).toBe("Karakai Jouzu no Takagi-san 3") + }) + + test("Tokyo Mew Mew New ♡ 2nd Season", async () => { + const anime = await getAmimeMyAnimeList( + "Tokyo Mew Mew New ♡ 2nd Season", + "東京ミュウミュウ にゅ~♡" + ) + + expect(anime.id).toBe(53097) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/53097/Tokyo_Mew_Mew_New_♡_2nd_Season" + ) + expect(anime.name).toBe("Tokyo Mew Mew New ♡ 2nd Season") + }) + + test("Tokyo Mew Mew New ♡", async () => { + const anime = await getAmimeMyAnimeList( + "Tokyo Mew Mew New ♡", + "東京ミュウミュウ にゅ~♡" + ) + + expect(anime.id).toBe(41589) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/41589/Tokyo_Mew_Mew_New_♡" + ) + expect(anime.name).toBe("Tokyo Mew Mew New ♡") + }) + + test("Tokyo Mew Mew", async () => { + const anime = await getAmimeMyAnimeList( + "Tokyo Mew Mew", + "Tokyo Mew Mew, Mew Mew Power" + ) + + expect(anime.id).toBe(687) + expect(anime.type).toBe("anime") + expect(anime.url).toBe("https://myanimelist.net/anime/687/Tokyo_Mew_Mew") + expect(anime.name).toBe("Tokyo Mew Mew") + }) + + test("Isekai wa Smartphone to Tomo ni.", async () => { + const anime = await getAmimeMyAnimeList( + "Isekai wa Smartphone to Tomo ni.", + "TIn Another World With My Smartphone, In a Different World with a Smartphone" + ) + + expect(anime.id).toBe(35203) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/35203/Isekai_wa_Smartphone_to_Tomo_ni" + ) + expect(anime.name).toBe("Isekai wa Smartphone to Tomo ni.") + }) + + test("Isekai wa Smartphone to Tomo ni. 2nd Season", async () => { + const anime = await getAmimeMyAnimeList( + "Isekai wa Smartphone to Tomo ni. 2nd Season", + "In Another World With My Smartphone 2, In Another World With My Smartphone 2nd Season, In a Different World with a Smartphone." + ) + + expect(anime.id).toBe(51632) + expect(anime.type).toBe("anime") + expect(anime.url).toBe( + "https://myanimelist.net/anime/51632/Isekai_wa_Smartphone_to_Tomo_ni_2" + ) + expect(anime.name).toBe("Isekai wa Smartphone to Tomo ni. 2") + }) + }) +}) diff --git a/src/apis/runs/myanimelist/search.ts b/src/apis/runs/myanimelist/search.ts new file mode 100644 index 00000000..4f02b84d --- /dev/null +++ b/src/apis/runs/myanimelist/search.ts @@ -0,0 +1,205 @@ +// first in query id anime + +import MiniSearch from "minisearch" +import type MyAnimeListSearchParser from "src/apis/parser/myanimelist/search" +import { get } from "src/logic/http" + +import { useCache } from "../../useCache" +import Worker from "../../workers/myanimelist/search?worker" +import { PostWorker } from "../../wrap-worker" + +const PREFIX_SEASON = "mùa|season|part|ss" +const BEFORE_SEASON = "ss|\\wd" + +async function runRawSearch(query: string) { + return await useCache(`myanimelist/search/${query}`, async () => { + const { data: html } = await get( + `https://myanimelist.net/anime.php?cat=anime&q=${encodeURIComponent( + query + )}&type=0&score=0&status=0&p=0&r=0&sm=0&sd=0&sy=0&em=0&ed=0&ey=0&c%5B%5D=a&c%5B%5D=b&c%5B%5D=c&c%5B%5D=f` + ) + + if (import.meta.env.MODE === "test") { + return import("../../parser/myanimelist/search").then((res) => + res.default(html) + ) + } + + return PostWorker(Worker, html) + }) +} + +const createRegExpGetSeason = (num: string | "\\d+") => + new RegExp( + "(?:" + + `(?:(?:${PREFIX_SEASON})\\s*(${num}))` + + "|" + + `(?:(${num})(?:${BEFORE_SEASON}))(?:\\s+(?:season))` + + ")$", + "i" + ) +const defRegExpGetSeason = createRegExpGetSeason("\\d+") + +const basicExactOthername = (name: string, othername: string): boolean => { + // console.log({ name, othername, e: name === othername }) + + // check in only + if (othername === name) return true + + // check in first other name + if (othername.startsWith(name + ",")) return true + + // check inside other name + if (othername.includes("," + name + ",")) return true + if (othername.includes(", " + name + ",")) return true + + // check in last other name + if (othername.endsWith(", " + name)) return true + if (othername.endsWith("," + name)) return true + + return false +} + +function sliceOthername(othername: string[], size: number, max: number) { + // eslint-disable-next-line functional/no-let + let index = 0 + // eslint-disable-next-line functional/no-let + let off = (othername.length - 1) * 2 // is space for ", " + while (size + off > max) { + size -= othername[index].length + off -= 2 + index++ + } + + return othername.slice(index) +} + +const rPrefixSeason = new RegExp(`${PREFIX_SEASON} (\\d+)`, "gi") +const rBeforeSeason = new RegExp(`(\\d+)${BEFORE_SEASON}\\s+(?:season)?`, "gi") +export function searchShared( + name: string, + othername: string, + fetcher: (keyword: string) => Promise +): Promise { + const nameRmd = name + .replace(rPrefixSeason, " $1") + .replace(rBeforeSeason, "$1") + const otherRmd = othername + .replace(rPrefixSeason, " $1") + .replace(rBeforeSeason, "$1") + + const query = + nameRmd.slice(0, 100) + + (nameRmd.length > 100 + ? "" + : " " + + sliceOthername( + otherRmd.split(",").filter((item) => item.trim()), + otherRmd.length, + 100 - nameRmd.length - 1 /** 1 is val of space */ + ).join(",")) + + const keyword = query + console.log({ keyword }, keyword.length) + + return fetcher(keyword).then((anime) => { + // console.log(keyword) + // console.dir(data, { depth: null }) + // eslint-disable-next-line functional/no-throw-statements + if (anime.length === 0) throw new Error("not_found") + + const ss = name.match(defRegExpGetSeason)?.slice(1).filter(Boolean).at(-1) + const regexpSeasonSS = createRegExpGetSeason(ss ?? "\\d+") + + name = name.toLowerCase().trim() + const nameRmdSeason = ss + ? name.replace(defRegExpGetSeason, "").trim() + : name + const matchByName = anime.find((item) => { + const itemName = ss + ? item.name.replace(defRegExpGetSeason, "").toLowerCase().trim() + : item.name.toLowerCase() + + if (name === itemName || nameRmdSeason === itemName) { + return ss + ? !!item.name.match(regexpSeasonSS) + : !item.name.match(regexpSeasonSS) + } + + return false + }) + if (matchByName) return matchByName + + const othernameRmdSeason = ss + ? othername + .split(",") + .map((name) => { + return name + .trim() + .replace(defRegExpGetSeason, "") + .toLowerCase() + .trim() + }) + .join(",") + : othername.toLowerCase().trim() + const matchByOthername = anime.find((item, id) => { + const itemName = ss + ? item.name.replace(defRegExpGetSeason, "").toLowerCase().trim() + : item.name.toLowerCase() + // console.log(item.name, defRegExpGetSeason, itemName) + + if (basicExactOthername(itemName, othernameRmdSeason)) { + return ss + ? !!item.name.match(regexpSeasonSS) + : !item.name.match(regexpSeasonSS) + } + + return false + }) + + if (matchByOthername) return matchByOthername + + const miniSearch = new MiniSearch({ + fields: ["name_lower"], // fields to index for full-text search + storeFields: Object.keys(anime[0]), // fields to return with search results + }) + + anime.forEach((item) => { + item.name_lower = item.name.trim().toLowerCase() + }) + // Index all documents + miniSearch.addAll(anime) + + const result = miniSearch.search( + otherRmd + )[0] as unknown as (typeof anime)[0] + console.log(otherRmd, result) + // console.log(otherRmd, result.slice(0, 10)) + // .filter((item) => { + // return ss + // ? !!item.name.match(regexpSeasonSS) + // : !item.name.match(regexpSeasonSS) + // }) as unknown as Awaited>[0] + if (result) return result + + // eslint-disable-next-line functional/no-throw-statements + throw new Error("not_found") + }) +} + +export async function getAmimeMyAnimeList( + name: string, + othername: string +): Promise[0]> { + try { + return searchShared(name, othername, runRawSearch) + } catch (err) { + if ((err as Error | null)?.message?.startsWith("Unexpected token ")) { + // limit request + await new Promise((resolve) => setTimeout(resolve, 5000)) + return getAmimeMyAnimeList(name, othername) + } + + throw err + } +} diff --git a/src/apis/runs/translate.ts b/src/apis/runs/translate.ts new file mode 100644 index 00000000..43a08998 --- /dev/null +++ b/src/apis/runs/translate.ts @@ -0,0 +1,27 @@ +import { post } from "src/logic/http"; + +export function translateText(text: string[], from: string, to: string) { + return post("https://translate.google.it/translate_a/single", + `client=gtx&dt=t&dt=bd&dj=1&source=input&q=${encodeURIComponent(text.join("\n"))}&sl=${from}&tl=${to}&hl=en`,{ + "content-type": "application/x-www-form-urlencoded", + }).then(res => JSON.parse(res.data )as { + sentences: { + trans: string; + orig: string; + backend: number; + model_specification: {}[]; + translation_engine_debug_info: ({ + model_tracking: { + checkpoint_md5: string; + launch_doc: string; + }; + has_untranslatable_chunk?: undefined; + } | { + has_untranslatable_chunk: boolean; + model_tracking?: undefined; + })[]; + }[]; + src: string; + spell: {}; +}).then(data => data.sentences.map(item => item.trans)) +} diff --git a/src/apis/workers/myanimelist/episodes.ts b/src/apis/workers/myanimelist/episodes.ts new file mode 100644 index 00000000..672d3861 --- /dev/null +++ b/src/apis/workers/myanimelist/episodes.ts @@ -0,0 +1,4 @@ +import { WrapWorker } from "../..//wrap-worker" +import Episodes from "../../parser/myanimelist/episodes" + +WrapWorker(Episodes) diff --git a/src/apis/workers/myanimelist/search.ts b/src/apis/workers/myanimelist/search.ts new file mode 100644 index 00000000..86ea8cd0 --- /dev/null +++ b/src/apis/workers/myanimelist/search.ts @@ -0,0 +1,4 @@ +import { WrapWorker } from "../..//wrap-worker" +import Search from "../../parser/myanimelist/search" + +WrapWorker(Search) diff --git a/src/boot/installed-extension.ts b/src/boot/installed-extension.ts index cc6ee22d..49f5cc1c 100644 --- a/src/boot/installed-extension.ts +++ b/src/boot/installed-extension.ts @@ -1,13 +1,13 @@ import { i18n } from "src/boot/i18n" import { ref, watch } from "vue" -const installed = ref() +const installed = ref(typeof window.Http === 'object') setTimeout(() => { if (!installed.value) installed.value = false }, 5_0000) // eslint-disable-next-line functional/no-let -let Http: Http +let Http: Http = window.Http Object.defineProperty(window, "Http", { get() { // console.log("================= set Http ==================") diff --git a/src/logic/http.ts b/src/logic/http.ts index 9bb5e710..7e2942f9 100644 --- a/src/logic/http.ts +++ b/src/logic/http.ts @@ -64,7 +64,7 @@ export async function get< export async function post( url: string, - data: Record, + data: Record | string, headers?: Record ) { console.log("post: ", { @@ -92,7 +92,7 @@ export async function post( ) const response = (await window.Http.post({ - url: C_URL + url + "#animevsub-vsub", + url: url.includes("://") ? url : C_URL + url + "#animevsub-vsub", headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", diff --git a/src/pages/phim/_season.vue b/src/pages/phim/_season.vue index 4150ae81..c56d3810 100644 --- a/src/pages/phim/_season.vue +++ b/src/pages/phim/_season.vue @@ -470,6 +470,9 @@ import type { ResponseDataSeasonPending, ResponseDataSeasonSuccess, } from "./response-data-season" +import { getAmimeMyAnimeList } from "src/apis/runs/myanimelist/search" +import { getEpisodesMyAnimeList } from "src/apis/runs/myanimelist/episodes" +import { translateText } from "src/apis/runs/translate" // ================ follow ================ // ======================================================= // import SwipableBottom from "components/SwipableBottom.vue" @@ -612,6 +615,25 @@ watch( immediate: true, } ) +watch(data, async data => { + if (!data) return + + console.time('time load info episodes') + const info = await getAmimeMyAnimeList(data.name, data.othername) + + console.log("%cinfo: ", 'color: white; background-color: green', info) + + const infoEpisodes = await getEpisodesMyAnimeList(info.url) + + console.log("%cepisodes: ", 'color: white; background-color: green', infoEpisodes) + console.timeEnd('time load info episodes') + + console.time('time translate') + const nameEpisodesTranslated = await translateText(infoEpisodes.map(item=>item.name), 'en', 'vi') + + console.log("%cnameEpisodesTranslated: ", 'color: white; background-color: green',nameEpisodesTranslated) + console.timeEnd('time translate') +}) watch( [progressWatchStore, () => authStore.user_data], diff --git a/vitest.config.ts b/vitest.config.ts index ea99592f..1475dbe0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,16 @@ +import path from "path" import { defineConfig } from "vitest/config" export default defineConfig({ + resolve: { + alias: { + src: path.resolve(__dirname, "src"), + boot: path.resolve(__dirname, "src/boot"), + stores: path.resolve(__dirname, "src/stores"), + }, + }, test: { - setupFiles: ["@vitest/web-worker"], + setupFiles: ["@vitest/web-worker", "./setup.vitest.ts"], environment: "jsdom", globals: true, },