const torrentRx = /(magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent){1}/i const imageRx = /\.(jpeg|jpg|gif|png|webp)/ window.addEventListener('paste', async e => { // WAIT image lookup on paste, or add torrent on paste const item = e.clipboardData.items[0] if (item && item.type.indexOf('image') === 0) { e.preventDefault() const reader = new FileReader() reader.onload = e => { traceAnime(e.target.result, 'uri') } reader.readAsDataURL(item.getAsFile()) } else if (item && item.type === 'text/plain') { item.getAsString(text => { if (torrentRx.exec(text)) { e.preventDefault() search.value = '' client.addTorrent(text, {}) } else if (imageRx.exec(text)) { e.preventDefault() search.value = '' traceAnime(text) } }) } else if (item && item.type === 'text/html') { item.getAsString(text => { const img = new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img')[0] if (img) { e.preventDefault() search.value = '' traceAnime(img.src) } }) } }) if (searchParams.get('link')) { traceAnime(searchParams.get('link')) window.location = '/app/#home' } function traceAnime (image, type) { // WAIT lookup logic halfmoon.initStickyAlert({ content: `Looking up anime for image.
` }) let options let 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) { const res = await alRequest({ method: 'SearchIDSingle', id: result.docs[0].anilist_id }) viewAnime(res.data.Media) } else { halfmoon.initStickyAlert({ content: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.', title: 'Search Failed', alertType: 'alert-danger', fillType: '' }) } }) } function searchBox () { // make searchbox behave nicely search.placeholder = search.value searchAnime(search.value) search.value = '' document.location.hash = '#browse' } // events navNowPlaying.onclick = () => { viewAnime(playerData.nowPlaying[0]) } // AL lookup logic async function alRequest (opts) { let query const variables = { type: 'ANIME', sort: opts.sort || 'TRENDING_DESC', page: opts.page || 1, perPage: opts.perPage || 30, status_in: opts.status_in || '[CURRENT,PLANNING]', chunk: opts.chunk || 1, perchunk: opts.perChunk || 30 } const options = { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: query, variables: variables }) } const queryObjects = ` id title { romaji english native userPreferred } description( asHtml: true ) season seasonYear format status episodes duration averageScore genres coverImage { extraLarge medium color } countryOfOrigin isAdult bannerImage synonyms nextAiringEpisode { timeUntilAiring episode } trailer { id site } streamingEpisodes { title thumbnail } relations { edges { relationType(version:2) node { id title { userPreferred } coverImage { medium } type status format episodes } } }` if (opts.status) variables.status = opts.status if (localStorage.getItem('ALtoken')) options.headers.Authorization = localStorage.getItem('ALtoken') if (opts.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 (opts.method === 'SearchName') { variables.search = opts.name query = ` query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus) { Page (page: $page, perPage: $perPage) { media(type: $type, search: $search, sort: $sort, status: $status) { ${queryObjects} } } }` } else if (opts.method === 'SearchIDSingle') { variables.id = opts.id query = ` query ($id: Int, $type: MediaType) { Media (id: $id, type: $type){ ${queryObjects} } }` } else if (opts.method === 'Viewer') { query = ` query { Viewer { avatar { medium }, name, id } }` } else if (opts.method === 'UserLists') { variables.id = opts.id query = ` query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [MediaListStatus]){ Page (page: $page, perPage: $perPage) { mediaList (userId: $id, type: $type, status_in: $status_in) { media { ${queryObjects} } } } }` } else if (opts.method === 'SearchIDStatus') { variables.id = alID variables.mediaId = opts.id query = ` query ($id: Int, $mediaId: Int){ MediaList(userId: $id, mediaId: $mediaId) { status progress repeat } }` } else if (opts.method === 'Genre') { variables.genre = opts.genre query = ` query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $genre: String) { Page (page: $page, perPage: $perPage) { media(type: $type, sort: $sort, genre: $genre) { ${queryObjects} } } }` } else if (opts.method === 'AiringSchedule') { const date = new Date() const diff = date.getDay() >= 1 ? date.getDay() - 1 : 6 - date.getDay() date.setDate(date.getDate() - diff) date.setHours(0, 0, 0, 0) variables.from = date.getTime() / 1000 variables.to = (date.getTime() + 7 * 24 * 60 * 60 * 1000) / 1000 console.log(variables) query = ` query ($page: Int, $perPage: Int, $from: Int, $to: Int) { Page (page: $page, perPage: $perPage) { airingSchedules(airingAt_greater: $from, airingAt_lesser: $to) { episode timeUntilAiring airingAt media{ ${queryObjects} } } } }` } options.body = JSON.stringify({ query: query, variables: variables }) const res = await fetch('https://graphql.anilist.co', options).catch((error) => console.error(error)) const json = await res.json() console.log(json) return json } async function alEntry () { if (playerData.nowPlaying && playerData.nowPlaying[0] && localStorage.getItem('ALtoken')) { const res = await alRequest({ method: 'SearchIDStatus', id: playerData.nowPlaying[0].id }) if ((res.errors && res.errors[0].status === 404) || res.data.MediaList.progress <= parseInt(playerData.nowPlaying[1])) { const query = ` mutation ($id: Int, $status: MediaListStatus, $episode: Int, $repeat: Int) { SaveMediaListEntry (mediaId: $id, status: $status, progress: $episode, repeat: $repeat) { id status progress repeat } }` const variables = { repeat: 0, id: playerData.nowPlaying[0].id, status: 'CURRENT', episode: parseInt(playerData.nowPlaying[1]) } if (parseInt(playerData.nowPlaying[1]) === playerData.nowPlaying[0].episodes) { variables.status = 'COMPLETED' if (res.data.MediaList.status === 'COMPLETED') { variables.repeat = res.data.MediaList.repeat + 1 } } const 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).catch((error) => console.error(error)) } } } let alResponse async function searchAnime (a) { // search bar functionality const frag = document.createDocumentFragment() const browse = document.querySelector('.browse') browse.innerHTML = '' browse.appendChild(skeletonCard) a ? alResponse = await alRequest({ method: 'SearchName', name: a }) : alResponse = await alRequest({ method: 'Trending' }) try { alResponse.data.Page.media.forEach(media => { const template = cardCreator({ media: media }) template.onclick = () => { viewAnime(media) } frag.appendChild(template) }) } catch (e) { console.error(e) } browse.innerHTML = '' browse.appendChild(frag) } // these really shouldnt be global const detailsfrag = document.createDocumentFragment() const 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.innerHTML = media.title.userPreferred viewDesc.innerHTML = media.description || '' viewDetails.innerHTML = '' detailsCreator(media) viewDetails.appendChild(detailsfrag) if (media.nextAiringEpisode) { const temp = document.createElement('p') temp.innerHTML = `Airing
Episode ${media.nextAiringEpisode.episode}: ${countdown(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 = '' const frag = document.createDocumentFragment() media.relations.edges.forEach(edge => { const 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') const res = await alRequest({ method: 'SearchIDSingle', id: edge.node.id }) viewAnime(res.data.Media) } frag.appendChild(template) }) viewRelationsGallery.appendChild(frag) } else { viewRelationsGallery.classList.add('d-none') } viewSynonym.onclick = () => { store[viewSynonymText.value] = media viewSynonymText.value = '' localStorage.setItem('store', JSON.stringify(store)) } episodes.innerHTML = '' if (media.streamingEpisodes.length) { viewEpisodesWrapper.classList.add('remove') const frag = document.createDocumentFragment() media.streamingEpisodes.forEach(episode => { const 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') } } trailerClose.onclick = () => { trailerVideo.src = '' } 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 => { const 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 countdown (s) { const d = Math.floor(s / (3600 * 24)) s -= d * 3600 * 24 const h = Math.floor(s / 3600) s -= h * 3600 const m = Math.floor(s / 60) s -= m * 60 const tmp = []; (d) && tmp.push(d + 'd'); (d || h) && tmp.push(h + 'h'); (d || h || m) && tmp.push(m + 'm') return tmp.join(' ') } function cardCreator (opts) { const template = document.createElement('div') template.classList.add('card', 'm-0', 'p-0') if (opts?.media) { template.innerHTML = `
${opts.media.title.userPreferred}${opts.episode ? ' - ' + opts.episode : ''}
${opts.schedule && opts.media.nextAiringEpisode ? "EP " + opts.media.nextAiringEpisode.episode + ' in ' + countdown(opts.media.nextAiringEpisode.timeUntilAiring) + '' : ''}

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

${opts.media.description}
${opts.media.genres.map(key => (`${key} `)).join('')}
` } else { template.innerHTML = `
${opts?.parseObject ? `
${opts.parseObject.anime_title + ' - ' + opts.parseObject.episode_number}
` : '

'}

` } return template } const skeletonCard = cardCreator({}) const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser()) async function nyaaSearch (media, episode) { if (parseInt(episode) < 10) { episode = `0${episode}` } const table = document.querySelector('tbody.results') const 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.innerHTML = '' table.appendChild(results) halfmoon.toggleModal('tsearch') } } const exclusions = { edge: ['DTS'], chromium: ['DTS', 'AC3', 'HEVC', 'x265', 'H.265', '.m2ts', '.ts'], firefox: ['DTS', 'AC3', 'HEVC', 'x265', 'H.265', '.m2ts', '.ts', '.3gp', '.mkv'] } async function nyaaRss (media, episode) { const frag = document.createDocumentFragment() const ep = (media.status === 'FINISHED' && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"Batch"|"Complete"|"+${episode}+"|"+${episode}v"` : `"+${episode}+"|"+${episode}v"` const 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}"-(${exclusions[userBrowser].join('|')})`) res = await fetch(url) await res.text().then((xmlTxt) => { try { const doc = DOMPARSER(xmlTxt, 'text/xml') if (settings.torrent2 && doc.querySelectorAll('infoHash')[0]) { addTorrent(doc.querySelectorAll('infoHash')[0].innerHTML, { media: media, episode: episode }) halfmoon.toggleModal('tsearch') } doc.querySelectorAll('item').forEach((item, index) => { const i = item.querySelector.bind(item) const template = document.createElement('tr') template.innerHTML += ` ${(index + 1)} ${i('title').innerHTML} ${i('size').innerHTML} ${i('seeders').innerHTML} ${i('leechers').innerHTML} ${i('downloads').innerHTML} Play` template.onclick = () => { addTorrent(i('infoHash').innerHTML, { media: media, episode: episode }) } frag.appendChild(template) }) } catch (e) { console.error(e) } }) return frag } // resolve anime name based on file name and store it async function resolveFileMedia (opts) { // opts.fileName opts.method opts.isRelease const elems = await anitomyscript(opts.fileName) if (!store[elems.anime_title]) { // resolve name and shit let method, res if (opts.isRelease) { method = { name: elems.anime_title, method: 'SearchName', perPage: 1, status: 'RELEASING', sort: 'TRENDING_DESC' } // START_DATE_DESC // maybe releases should include this and last season? idfk } else { method = { name: elems.anime_title, method: opts.method, perPage: 1 } } res = await alRequest(method) if (!res.data.Page.media[0]) { method.name = method.name.replace(' (TV)', '').replace(` (${new Date().getFullYear()})`, '').replace('-', '').replace('S2', '2') // this needs to be improved!!! method.status = undefined res = await alRequest(method) } if (res.data.Page.media[0]) store[elems.anime_title] = res.data.Page.media[0] } else { store[elems.anime_title] = await alRequest({ id: store[elems.anime_title].id, method: 'SearchIDSingle' }).then(res => res.data.Media) } let episode; let media = store[elems.anime_title] // resolve episode, if movie, dont. if ((media?.format !== 'MOVIE' || (media.episodes || media.nextAiringEpisode.episode)) && elems.episode_number) { async function resolveSeason (opts) { // opts.media, opts.episode, opts.increment, opts.offset let epMin, epMax if (opts.episode.constructor === Array) { // support batch episode ranges epMin = Number(opts.episode[0]) epMax = Number(opts.episode[opts.episode.length - 1]) } else { epMin = epMax = Number(opts.episode) } let tempMedia, increment if (opts.media.relations.edges.some(edge => edge.relationType === 'PREQUEL' && (edge.node.format === 'TV' || 'TV_SHORT')) && !opts.increment) { // media has prequel and we dont want to move up in the tree tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'PREQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node } else if (opts.media.relations.edges.some(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))) { // media doesnt have prequel, or we want to move up in the tree tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node increment = true } if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) > (media.episodes || media.nextAiringEpisode.episode)) { // episode is still out of bounds const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id }) await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment }) } else if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) <= (media.episodes || media.nextAiringEpisode.episode) && epMin - (opts.offset + tempMedia.episodes) > 0) { // episode is in range, seems good! overwriting media to count up "seasons" if (opts.episode.constructor === Array) { episode = `${elems.episode_number[0] - (opts.offset + tempMedia.episodes)} - ${elems.episode_number[elems.episode_number.length - 1] - (opts.offset + tempMedia.episodes)}` } else { episode = opts.episode - (opts.offset + tempMedia.episodes) } if (opts.increment) { const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id }) media = nextEdge.data.Media } } else { console.log('error in parsing!', opts.media, tempMedia) // something failed, most likely couldnt find an edge or processing failed, force episode number even if its invalid/out of bounds, better than nothing if (opts.episode.constructor === Array) { episode = `${Number(elems.episode_number[0])} - ${Number(elems.episode_number[elems.episode_number.length - 1])}` } else { episode = Number(opts.episode) } } } if (elems.episode_number.constructor === Array) { // is an episode range if (parseInt(elems.episode_number[0]) === 1) { // if it starts with #1 and overflows then it includes more than 1 season in a batch, cant fix this cleanly, name is parsed per file basis so this shouldnt be an issue episode = `${elems.episode_number[0]} - ${elems.episode_number[elems.episode_number.length - 1]}` } else { if ((media?.episodes || media?.nextAiringEpisode?.episode) && parseInt(elems.episode_number[elems.episode_number.length - 1]) > (media.episodes || media.nextAiringEpisode.episode)) { // if highest value is bigger than episode count or latest streamed episode +1 for safety, parseint to math.floor a number like 12.5 - specials - in 1 go await resolveSeason({ media: media, episode: elems.episode_number, offset: 0 }) } else { // cant find ep count or range seems fine episode = `${Number(elems.episode_number[0])} - ${Number(elems.episode_number[elems.episode_number.length - 1])}` } } } else { if ((media?.episodes || media?.nextAiringEpisode?.episode) && parseInt(elems.episode_number) > (media.episodes || media.nextAiringEpisode.episode)) { // value bigger than episode count await resolveSeason({ media: media, episode: elems.episode_number, offset: 0 }) } else { // cant find ep count or episode seems fine episode = Number(elems.episode_number) } } } return { media: media, episode: episode, parseObject: elems } } let store = JSON.parse(localStorage.getItem('store')) || {} store = store || {} function getRSSurl () { if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) { // add my own cors proxy for erai return settings.torrent4 === 'Erai-raws' ? new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1 + '-magnet') : new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1) } else { return settings.torrent4 + settings.torrent1 // add custom RSS } } async function releasesCards (items, frag, limit) { const mediaItems = [] for (let l = 0; l < (limit || items.length); l++) { const i = items[l].querySelector.bind(items[l]) mediaItems.push(resolveFileMedia({ fileName: i('title').innerHTML, method: 'SearchName', isRelease: true })) } await Promise.all(mediaItems).then(results => { results.forEach((mediaInformation, index) => { const o = items[index].querySelector.bind(items[index]) template = cardCreator(mediaInformation) template.onclick = () => client.addTorrent(o('link').innerHTML, { media: mediaInformation.media, episode: mediaInformation.episode }) frag.appendChild(template) }) }) localStorage.setItem('store', JSON.stringify(store)) } async function releasesRss (limit) { const frag = document.createDocumentFragment() await fetch(getRSSurl()).then(res => res.text().then(async xmlTxt => { try { const doc = DOMPARSER(xmlTxt, 'text/xml') const items = doc.querySelectorAll('item') await releasesCards(items, frag, limit) } catch (e) { console.error(e) } })) return frag } let alID // login icon async function loadAnime () { // await searchAnime() if (localStorage.getItem('ALtoken')) { alRequest({ method: '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 loadHomePage() }) } else { loadHomePage() home.classList.add('noauth') } } loadAnime()