const torrentRx = /(magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent){1}/i, imageRx = /\.(jpeg|jpg|gif|png|webp)/ window.addEventListener("paste", async e => { //WAIT image lookup on paste, or add torrent on paste let item = e.clipboardData.items[0]; if (item && item.type.indexOf("image") === 0) { e.preventDefault(); let blob = item.getAsFile(); let reader = new FileReader(); reader.onload = e => { traceAnime(e.target.result, "uri") }; reader.readAsDataURL(blob); } else if (item && item.type === "text/plain") { item.getAsString(text => { if (torrentRx.exec(text)) { e.preventDefault(); search.value = "" addTorrent(text); } else if (imageRx.exec(text)) { e.preventDefault(); search.value = "" traceAnime(text) } }) } else if (item && item.type === "text/html") { item.getAsString(text => { let domparser = new DOMParser(), doc = domparser.parseFromString(text, "text/html"), img = doc.querySelectorAll("img")[0] if (img) { e.preventDefault(); search.value = "" traceAnime(img.src) } }) } }) function traceAnime(image, type) { //WAIT lookup logic halfmoon.initStickyAlert({ content: `Looking Up Anime ${type == "uri" ? "" : `For ${image}`}` }) let options, url = `https://trace.moe/api/search?url=${image}` if (type == "uri") { options = { method: "POST", body: JSON.stringify({ image: image }), headers: { "Content-Type": "application/json" }, }, url = "https://trace.moe/api/search" } fetch(url, options).then((res) => res.json()) .then(async (result) => { if (result.docs[0].similarity >= 0.85) { let res = await alRequest(result.docs[0].anilist_id, "SearchIDSingle") viewAnime(res.data.Media) } else { console.log("no." + result.docs[0].similarity) } }); } function searchBox() { // make searchbox behave nicely search.placeholder = search.value searchAnime(search.value) search.value = "" document.location.hash = "#browse" } //events navNew.onclick = () => { releasesRss() } navTrending.onclick = () => { searchAnime() } navNowPlaying.onclick = () => { viewAnime(playerData.nowPlaying[0]) } navList.onclick = async () => { //user watchlist let browse = document.querySelector(".browse") browse.textContent = ''; browse.appendChild(skeletonCard) let res = await alRequest(alID, "UserLists"), entries = res.data.MediaListCollection.lists[0].entries.concat(res.data.MediaListCollection.lists[1].entries), frag = document.createDocumentFragment() try { entries.forEach(media => { let template = cardCreator(media.media) template.onclick = () => { viewAnime(media.media) } frag.appendChild(template) }) } catch (e) { console.error(e) } browse.textContent = ''; browse.appendChild(frag) } //AL lookup logic async function alRequest(searchName, method) { let query, variables = { type: "ANIME", sort: "TRENDING_DESC", page: 1, perPage: 50 }, options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ query: query, variables: variables }) }, queryObjects = ` id title { romaji english native userPreferred } description( asHtml: true ) season seasonYear format status episodes duration averageScore genres coverImage { extraLarge medium color } bannerImage synonyms nextAiringEpisode { timeUntilAiring episode } trailer { id site } streamingEpisodes { title thumbnail } relations { edges { relationType(version:2) node { id title { userPreferred } coverImage { medium } type status } } } ` if (localStorage.getItem("ALtoken")) { options.headers['Authorization'] = localStorage.getItem("ALtoken") } if (method == "Trending") { search.placeholder = "Search" query = ` query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType) { Page (page: $page, perPage: $perPage) { media(type: $type, sort: $sort) { ${queryObjects} } } }` } else if (method == "SearchReleasesSingle") { variables.search = searchName variables.perPage = 1 variables.status = "RELEASING" query = ` query ($page: Int, $sort: [MediaSort], $search: String, $type: MediaType, $status: MediaStatus) { Page (page: $page) { media (type: $type, search: $search, sort: $sort, status: $status) { ${queryObjects} } } }` } else if (method == "SearchName") { variables.search = searchName variables.sort = "TRENDING_DESC" query = ` query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String) { Page (page: $page, perPage: $perPage) { media(type: $type, search: $search, sort: $sort) { ${queryObjects} } } }` } else if (method == "SearchAnySingle") { variables.search = searchName variables.perPage = 1 variables.sort = "TRENDING_DESC" query = ` query ($page: Int, $sort: [MediaSort], $search: String, $type: MediaType) { Page (page: $page) { media (type: $type, search: $search, sort: $sort) { ${queryObjects} } } }` } else if (method == "SearchIDSingle") { variables.id = searchName query = ` query ($id: Int, $type: MediaType) { Media (id: $id, type: $type){ ${queryObjects} } }` } else if (method == "Viewer") { query = ` query { Viewer { avatar { medium }, name, id } } ` } else if (method == "UserLists") { variables.id = searchName query = ` query ($id: Int, $type: MediaType){ MediaListCollection (userId: $id, type: $type, forceSingleCompletedList: true, status_in: [CURRENT,PLANNING]) { lists { entries { media { ${queryObjects} } } } } } ` } options.body = JSON.stringify({ query: query, variables: variables }) let res = await fetch('https://graphql.anilist.co', options).catch((error) => console.error(error)), json = await res.json(); return json } function alEntry() { if (playerData.nowPlaying && playerData.nowPlaying[0] && localStorage.getItem("ALtoken")) { let query = ` mutation ($id: Int, $status: MediaListStatus, $episode: Int) { SaveMediaListEntry (mediaId: $id, status: $status, progress: $episode) { id status progress } }`, variables = { id: playerData.nowPlaying[0].id, status: "CURRENT", episode: parseInt(playerData.nowPlaying[1]) }, options = { method: 'POST', headers: { 'Authorization': 'Bearer ' + localStorage.getItem("ALtoken"), 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ query: query, variables: variables }) } fetch("https://graphql.anilist.co", options) } } let alResponse async function searchAnime(a) { //search bar functionality let frag = document.createDocumentFragment(), browse = document.querySelector(".browse") browse.textContent = ''; browse.appendChild(skeletonCard) a ? alResponse = await alRequest(a, "SearchName") : alResponse = await alRequest(a, "Trending") try { alResponse.data.Page.media.forEach(media => { let template = cardCreator(media) template.onclick = () => { viewAnime(media) } frag.appendChild(template) }) } catch (e) { console.error(e) } browse.textContent = ''; browse.appendChild(frag) } //these really shouldnt be global let detailsfrag = document.createDocumentFragment() let details = { averageScore: "Average Score", // duration: "Episode Duration", // episodes: "Episodes", // format: "Format", genres: "Genres", // season: "Season", // seasonYear: "Year", status: "Status", english: "English", romaji: "Romaji", native: "Native", synonyms: "Synonyms" } const episodeRx = /Episode (\d+) - (.*)/; // this is fucked beyond belief, this is why you use frameworks function viewAnime(media) { halfmoon.showModal("view") view.setAttribute("style", `background-image: url(${media.bannerImage}) !important`) viewImg.src = media.coverImage.extraLarge viewTitle.textContent = media.title.userPreferred viewDesc.innerHTML = media.description || "" viewDetails.innerHTML = "" detailsCreator(media) viewDetails.appendChild(detailsfrag) if (media.nextAiringEpisode) { let temp = document.createElement("p") temp.innerHTML = `Airing
Episode ${media.nextAiringEpisode.episode}: ${toTS(media.nextAiringEpisode.timeUntilAiring)}` viewDetails.prepend(temp) } viewSeason.innerHTML = `${(media.season ? media.season.toLowerCase() + " " : "") + (media.seasonYear ? media.seasonYear : "")}` viewMediaInfo.innerHTML = `${media.format ? "" + media.format + "" : ""}${media.episodes ? "" + media.episodes + " Episodes" : ""}${media.duration ? "" + media.duration + " Minutes" : ""}` viewPlay.onclick = () => { nyaaSearch(media, 1); halfmoon.toggleModal("view") } if (media.trailer) { viewTrailer.removeAttribute("disabled", "") viewTrailer.onclick = () => trailerPopup(media.trailer) } else { viewTrailer.setAttribute("disabled", "") } if (media.status == "NOT_YET_RELEASED") { viewPlay.setAttribute("disabled", "") } else { viewPlay.removeAttribute("disabled", "") } if (media.relations.edges.length) { viewRelationsGallery.classList.remove("d-none") viewRelationsGallery.innerHTML = "" let frag = document.createDocumentFragment() media.relations.edges.forEach(edge => { let template = document.createElement("div") template.classList.add("card", "m-0", "p-0") template.innerHTML = `

${edge.node.title.userPreferred}

${edge.relationType.toLowerCase()}

${edge.node.type.toLowerCase()}${edge.node.status.toLowerCase()}
` template.onclick = async () => { halfmoon.hideModal("view") let res = await alRequest(edge.node.id, "SearchIDSingle") viewAnime(res.data.Media) } frag.appendChild(template) }) viewRelationsGallery.appendChild(frag) } else { viewRelationsGallery.classList.add("d-none") } viewEpisodes.onclick = () => { viewEpisodesWrapper.classList.toggle("hidden") } viewSynonym.onclick = () => { store[viewSynonymText.value] = media viewSynonymText.value = "" localStorage.setItem("store", JSON.stringify(store)) } episodes.innerHTML = "" if (media.streamingEpisodes.length) { viewEpisodesWrapper.classList.add("hidden") viewEpisodes.removeAttribute("disabled", "") let frag = document.createDocumentFragment() media.streamingEpisodes.forEach(episode => { let temp = document.createElement("div") temp.classList.add("position-relative", "w-250", "rounded", "mr-10", "overflow-hidden", "pointer") temp.innerHTML = `
${episode.title}
` temp.onclick = () => { nyaaSearch(media, episodeRx.exec(episode.title)[1]); halfmoon.toggleModal("view") } frag.appendChild(temp) }) episodes.appendChild(frag) } else { viewEpisodesWrapper.classList.add("hidden") viewEpisodes.setAttribute("disabled", "") } } function trailerPopup(trailer) { trailerVideo.src = "" halfmoon.toggleModal("trailer") switch (trailer.site) { // should support the other possible sites too, but i cant find any examples case "youtube": trailerVideo.src = "https://www.youtube.com/embed/" + trailer.id break; } } //details list factory function detailsCreator(entry) { if (entry) { Object.entries(entry).forEach(value => { let template = document.createElement("p") if (typeof value[1] == 'object') { if (Array.isArray(value[1])) { if (details[value[0]] && value[1].length > 0) { template.innerHTML = `${details[value[0]]}
${value[1].map(key => (key)).join(', ')}` detailsfrag.appendChild(template) } } else { detailsCreator(value[1]) } } else { if (details[value[0]]) { template.innerHTML = `${details[value[0]]}
${value[1].toString()}` detailsfrag.appendChild(template) } } }) } } function cardCreator(media, name, episode) { let template = document.createElement("div") template.classList.add("card", "m-0", "p-0") if (media) { template.innerHTML = `
${media.title.userPreferred}${episode ? " - " + episode : ""}

${(media.format ? (media.format == "TV" ? "" + media.format + " Show" : "" + media.format.toLowerCase().replace(/_/g, " ")) : "") + ""} ${media.episodes ? "" + media.episodes + " Episodes" : media.duration ? "" + media.duration + " Minutes" : ""} ${media.status ? "" + media.status.toLowerCase().replace(/_/g, " ") + "" : ""} ${media.season || media.seasonYear ? "" + (!!media.season ? media.season.toLowerCase() + " " : "") + (media.seasonYear || "") + "" : ""}

${media.description}
${media.genres.map(key => (`${key} `)).join('')}
` } else { template.innerHTML = `
${name ? name + " - " + episode : ""}
` } return template } let skeletonCard = cardCreator() const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser()) async function nyaaSearch(media, episode) { if (parseInt(episode) < 10) { episode = `0${episode}` } let table = document.querySelector("tbody.results") let results = await nyaaRss(media, episode) if (results.children.length == 0) { halfmoon.initStickyAlert({ content: `Couldn't find torrent for ${media.title.userPreferred} Episode ${parseInt(episode)}! Try specifying a torrent manually.`, title: "Search Failed", alertType: "alert-danger", fillType: "" }) } else { table.textContent = "" table.appendChild(results) halfmoon.toggleModal("tsearch") } } async function nyaaRss(media, episode) { let frag = document.createDocumentFragment(), ep = (media.status == "FINISHED" && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"batch"|"Batch"|"complete"|"Complete"|"+01+"|"+01v"` : `"+${episode}+"|"+${episode}v"`, url = new URL(`https://miru.kirdow.com/request/?url=https://nyaa.si/?page=rss$c=1_2$f=${settings.torrent3 == true ? 2 : 0}$s=seeders$o=desc$q=(${[...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join(")|(")})${ep}"${settings.torrent1}"`) // console.log(`"${[...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join("\"|\"")}"${ep}"${settings.torrent1}"`) res = await fetch(url) await res.text().then((xmlTxt) => { try { let doc = DOMPARSER(xmlTxt, "text/xml") if (settings.torrent2 && doc.querySelectorAll("infoHash")[0]) { addTorrent(doc.querySelectorAll("infoHash")[0].textContent, media, episode) halfmoon.toggleModal("tsearch") } doc.querySelectorAll("item").forEach((item, index) => { let i = item.querySelector.bind(item) let template = document.createElement("tr") template.innerHTML += ` ${(index + 1)} ${i("title").textContent} ${i("size").textContent} ${i("seeders").textContent} ${i("leechers").textContent} ${i("downloads").textContent} Play` template.onclick = () => { addTorrent(i('infoHash').textContent, media, episode) } frag.appendChild(template) }) } catch (e) { console.error(e) } }) return frag } //resolve anime name based on torrent name and store it async function resolveName(name, method) { if (!store.hasOwnProperty(name) && !alResponse.data.Page.media.some(media => (Object.values(media.title).concat(media.synonyms).filter(name => name != null).includes(name) && ((store[name] = media) && true)))) { let res = await alRequest(name, method) if (!res.data.Page.media[0]) { res = await alRequest(name.replace(" (TV)", "").replace(` (${new Date().getFullYear()})`, ""), method) } if (settings.torrent7 && !res.data.Page.media[0] && method == "SearchReleasesSingle") { res = await alRequest(name, "SearchAnySingle") } store[name] = res.data.Page.media[0] } return store[name] } const nameParseRegex = { simple: /(\[.[^\]]*\]\ ?|\(.[^\)]*\)\ ?)?(.+?(?=\ \-\ \d{2,}|\ \–\ \d{2,}))?(\ \-\ |\ \–\ )?(\d{2,})?(.*)?/i, fallback: /((?:\[[^\]]*\])*)?\s*((?:[^\d\[\.](?!S\d))*)?\s*((?:S\d+[^\w\[]*E?)?[\d\-]*)\s*(.*)?/i } let store = JSON.parse(localStorage.getItem("store")) || {}, lastResult async function releasesRss() { let frag = document.createDocumentFragment(), releases = document.querySelector(".releases"), url if (Object.values(torrent4list.options).filter(item => item.value == settings.torrent4)[0]) { //add my own cors proxy for erai url = settings.torrent4 == "Erai-raws" ? new URL(Object.values(torrent4list.options).filter(item => item.value == settings.torrent4)[0].innerText + settings.torrent1 + "-magnet") : new URL(Object.values(torrent4list.options).filter(item => item.value == settings.torrent4)[0].innerText + settings.torrent1) } else { url = settings.torrent4 + settings.torrent1 // add custom RSS } let res = await fetch(url) await res.text().then(async (xmlTxt) => { try { let doc = DOMPARSER(xmlTxt, "text/xml") if (lastResult != doc) { releases.textContent = ''; releases.appendChild(skeletonCard) lastResult = doc let items = doc.querySelectorAll("item") for (let item of items) { let i = item.querySelector.bind(item), regexParse = nameParseRegex.simple.exec(i("title").textContent), episode if (!regexParse[2]) { regexParse = nameParseRegex.fallback.exec(i("title").textContent) episode = regexParse[3] } else { episode = regexParse[4] } let media = await resolveName(regexParse[2], "SearchReleasesSingle"), template = cardCreator(media, regexParse[2], episode) template.onclick = async () => { addTorrent(i('link').textContent, media, episode) let res = await alRequest(media.id, "SearchIDSingle") store[regexParse[2]] = res.data.Media // force updates entry data on play in case its outdated, needs to be made cleaner and somewhere else... } frag.appendChild(template) } releases.textContent = ''; releases.appendChild(frag) } } catch (e) { console.error(e) } }) localStorage.setItem("store", JSON.stringify(store)) } //latest releases auto-update setInterval(() => { if (document.location.hash == "#releases") { releasesRss() } }, 30000); async function loadAnime() { await searchAnime() releasesRss() } loadAnime() let alID // login icon if (localStorage.getItem("ALtoken")) { alRequest(undefined, "Viewer").then(result => { oauth.removeAttribute("href") oauth.setAttribute("data-title", `${result.data.Viewer.name}\nClick To Logout`) oauth.innerHTML = `` oauth.onclick = () => { localStorage.removeItem("ALtoken"); location.reload() } alID = result.data.Viewer.id }) }