feat: show errors in search when no results found

feat: file picker for subtitles in options
feat: drag/drop/paste subtitle files/text
fix: hide video on seek
fix: profile icon not centered
This commit is contained in:
ThaUnknown 2025-05-06 01:03:40 +02:00
parent d2335b1e15
commit b76ce678df
No known key found for this signature in database
15 changed files with 235 additions and 138 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.1.16",
"version": "6.1.17",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",
@ -23,7 +23,6 @@
"@sveltejs/kit": "^2.8.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/debug": "^4.1.12",
"@types/events": "^3.0.3",
"@types/semver": "^7.7.0",
"@urql/introspection": "^1.1.0",
"autoprefixer": "^10.4.20",

View file

@ -132,9 +132,6 @@ importers:
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@types/events':
specifier: ^3.0.3
version: 3.0.3
'@types/semver':
specifier: ^7.7.0
version: 7.7.0
@ -664,9 +661,6 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/events@3.0.3':
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -3023,8 +3017,6 @@ snapshots:
'@types/estree@1.0.6': {}
'@types/events@3.0.3': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}

1
src/app.d.ts vendored
View file

@ -139,7 +139,6 @@ declare global {
declare module '*.svelte' {
export default SvelteComponentTyped
}
}
declare module '*.svelte' {

View file

@ -36,16 +36,16 @@
function sanitiseTerms ({ video_term: video, audio_term: audio, video_resolution: resolution, source }: AnitomyResult) {
const terms = [...new Set([...video ?? [], ...audio ?? [], ...source ?? []].map(term => termMapping[term.toUpperCase() ?? '']).filter(t => t))] as Array<{text: string, color: string}>
if (resolution?.length) terms.unshift({ text: resolution[0]!, color: '#c6ec58' })
if (resolution.length) terms.unshift({ text: resolution[0]!, color: '#c6ec58' })
return terms
}
function simplifyFilename ({ video_term: video, audio_term: audio, video_resolution: resolution, file_name: name, release_group: group, file_checksum: checksum }: AnitomyResult) {
let simpleName = name[0]!
if (group?.length) simpleName = simpleName.replace(group[0]!, '')
if (resolution?.length) simpleName = simpleName.replace(resolution[0]!, '')
if (checksum?.length) simpleName = simpleName.replace(checksum[0]!, '')
if (group.length) simpleName = simpleName.replace(group[0]!, '')
if (resolution.length) simpleName = simpleName.replace(resolution[0]!, '')
if (checksum.length) simpleName = simpleName.replace(checksum[0]!, '')
for (const term of video ?? []) simpleName = simpleName.replace(term[0]!, '')
for (const term of audio ?? []) simpleName = simpleName.replace(term[0]!, '')
return simpleName.replace(/[[{(]\s*[\]})]/g, '').replace(/\s+/g, ' ').trim()
@ -260,6 +260,17 @@
</div>
</div>
</div>
{:else}
<div class='p-5 flex items-center justify-center w-full h-80'>
<div>
<div class='mb-3 font-bold text-4xl text-center '>
Ooops!
</div>
<div class='text-lg text-center text-muted-foreground'>
No results found.<br />Try specifying a torrent manually by pasting a magnet link or torrent file into the filter bar.
</div>
</div>
</div>
{/each}
{#each errors as error, i (i)}
<div class='p-5 flex items-center justify-center w-full h-80'>

View file

@ -144,6 +144,9 @@
</Tree.Sub>
</Tree.Item>
{/each}
<Tree.Item on:click={() => { subtitles.pickFile(); open = false }}>
<span>Add Subtitle File</span>
</Tree.Item>
</Tree.Sub>
</Tree.Item>
{/if}

View file

@ -615,8 +615,8 @@
<svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} />
<div class='w-full h-full relative content-center bg-black overflow-clip text-left' class:fitWidth bind:this={wrapper}>
<video class='w-full h-full' preload='auto' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband}
<div class='w-full h-full relative content-center bg-black overflow-clip text-left' class:fitWidth class:seeking bind:this={wrapper}>
<video class='w-full h-full' preload='auto' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband || seeking} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband}
use:createSubtitles
use:holdToFF={'pointer'}
crossorigin='anonymous'
@ -799,6 +799,10 @@
.fitWidth :global(.deband-canvas) {
object-fit: cover !important;
}
.seeking :global(.deband-canvas) {
opacity: 0 !important;
}
.gradient {
background: linear-gradient(to top, oklab(0 0 0 / 0.85) 0%, oklab(0 0 0 / 0.7) 35%, oklab(0 0 0 / 0) 100%);
}

View file

@ -6,16 +6,7 @@ import type { AnitomyResult } from 'anitomyscript'
import type { ResultOf } from 'gql.tada'
import { client, episodes, type Media } from '$lib/modules/anilist'
export const subtitleExtensions = ['srt', 'vtt', 'ass', 'ssa', 'sub', 'txt']
export const subRx = new RegExp(`.(${subtitleExtensions.join('|')})$`, 'i')
export const videoExtensions = ['3g2', '3gp', 'asf', 'avi', 'dv', 'flv', 'gxf', 'm2ts', 'm4a', 'm4b', 'm4p', 'm4r', 'm4v', 'mkv', 'mov', 'mp4', 'mpd', 'mpeg', 'mpg', 'mxf', 'nut', 'ogm', 'ogv', 'swf', 'ts', 'vob', 'webm', 'wmv', 'wtv']
export const videoRx = new RegExp(`.(${videoExtensions.join('|')})$`, 'i')
// freetype supported
export const fontExtensions = ['ttf', 'ttc', 'woff', 'woff2', 'otf', 'cff', 'otc', 'pfa', 'pfb', 'pcf', 'fnt', 'bdf', 'pfr', 'eot']
export const fontRx = new RegExp(`.(${fontExtensions.join('|')})$`, 'i')
import { videoRx } from '$lib/utils'
export type ResolvedFile = TorrentFile & {metadata: { episode: string | number | undefined, parseObject: AnitomyResult, media: Media, failed: boolean }}
@ -41,12 +32,12 @@ export async function resolveFilesPoorly (promise: Promise<{media: Media, id: st
...file,
metadata: resolved.find(({ parseObject }) => file.name.includes(parseObject.file_name[0]!))
}
}).filter(file => file.metadata && !TYPE_EXCLUSIONS.includes(file.metadata.parseObject.anime_type?.[0]?.toUpperCase() ?? '')) as ResolvedFile[] // assertion because of file metadata
}).filter(file => file.metadata && !TYPE_EXCLUSIONS.includes(file.metadata.parseObject.anime_type[0]?.toUpperCase() ?? '')) as ResolvedFile[] // assertion because of file metadata
let targetAnimeFiles = resolvedFiles.filter(file => file.metadata.media.id && file.metadata.media.id === list.media.id)
if (!targetAnimeFiles.length) {
const max = highestOccurence(resolvedFiles, file => file.metadata.parseObject.anime_title?.[0] ?? '').metadata.parseObject.anime_title
const max = highestOccurence(resolvedFiles, file => file.metadata.parseObject.anime_title[0] ?? '').metadata.parseObject.anime_title
targetAnimeFiles = resolvedFiles.filter(file => file.metadata.parseObject.anime_title === max)
}
@ -151,7 +142,7 @@ const AnimeResolver = new class AnimeResolver {
animeNameCache: Record<string, number> = {}
getCacheKeyForTitle (obj: AnitomyResult): string {
let key = obj.anime_title?.[0] ?? ''
let key = obj.anime_title[0] ?? ''
if (obj.anime_year) key += obj.anime_year[0]
return key
}
@ -199,7 +190,7 @@ const AnimeResolver = new class AnimeResolver {
if (!parseObjects.length) return
const titleObjects = parseObjects.map(obj => {
const key = this.getCacheKeyForTitle(obj)
const titleObjects: Array<{key: string, title: string, year?: string, isAdult: boolean}> = this.alternativeTitles(obj.anime_title?.[0] ?? '').map(title => ({ title, year: obj.anime_year?.[0], key, isAdult: false }))
const titleObjects: Array<{key: string, title: string, year?: string, isAdult: boolean}> = this.alternativeTitles(obj.anime_title[0] ?? '').map(title => ({ title, year: obj.anime_year[0], key, isAdult: false }))
// @ts-expect-error cba fixing this for now, but this is correct
titleObjects.push({ ...titleObjects.at(-1), isAdult: true })
return titleObjects

View file

@ -2,13 +2,12 @@ import JASSUB, { type ASS_Event as ASSEvent } from 'jassub'
import { writable } from 'simple-store-svelte'
import { get } from 'svelte/store'
import { fontRx, type ResolvedFile } from './resolver'
import type { ResolvedFile } from './resolver'
import type { TorrentFile } from '../../../../app'
import native from '$lib/modules/native'
import { settings, SUPPORTS } from '$lib/modules/settings'
import { HashMap } from '$lib/utils'
import { fontRx, HashMap, subRx, subtitleExtensions, toTS } from '$lib/utils'
// import { toTS } from '$lib/utils'
const defaultHeader = `[Script Info]
@ -38,6 +37,8 @@ export default class Subtitles {
_tracks = writable<Record<number | string, { events: HashMap<{ text: string, time: number, duration: number, style?: string }, ASSEvent>, meta: { language?: string, type: string, header: string, number: string, name?: string }, styles: Record<string | number, number> }>>({})
ctrl = new AbortController()
constructor (video: HTMLVideoElement, files: TorrentFile[], selected: ResolvedFile) {
this.video = video
this.selected = selected
@ -98,6 +99,66 @@ export default class Subtitles {
}
}
})
video.parentElement!.addEventListener('drop', e => this.handleTransfer(e), { signal: this.ctrl.signal })
video.parentElement!.addEventListener('paste', e => this.handleTransfer(e), { signal: this.ctrl.signal })
video.parentElement!.addEventListener('dragover', e => e.preventDefault(), { signal: this.ctrl.signal })
}
async handleTransfer (e: { dataTransfer?: DataTransfer | null, clipboardData?: DataTransfer | null } & Event) {
e.preventDefault()
const promises = [...(e.dataTransfer ?? e.clipboardData)!.items].map(item => {
const type = item.type
return new Promise<File>(resolve => item.kind === 'string' ? item.getAsString(text => resolve(new File([text], 'Subtitle.txt', { type }))) : resolve(item.getAsFile()!))
})
for (const file of await Promise.all(promises)) {
if (subRx.test(file.name)) this.addSingleSubtitleFile(file)
}
}
pickFile () {
const input = document.createElement('input')
input.type = 'file'
input.accept = subtitleExtensions.map(ext => '.' + ext).join(',')
input.multiple = true
input.addEventListener('change', async () => {
for (const file of Array.from(input.files ?? [])) {
if (subRx.test(file.name)) this.addSingleSubtitleFile(file)
}
})
input.click()
}
async addSingleSubtitleFile (file: File) {
// lets hope there's no more than 100 subtitle tracks in a file
const trackNumber = 1000 + Object.keys(this._tracks.value).length
const dot = file.name.lastIndexOf('.')
const extension = file.name.substring(dot + 1).toLowerCase()
if (!subtitleExtensions.includes(extension)) return
const filename = file.name.slice(0, dot)
// sub name could contain video name with or without extension, possibly followed by lang, or not.
const name = filename.includes(this.selected.name)
? filename.replace(this.selected.name, '')
: filename.replace(this.selected.name.slice(0, this.selected.name.lastIndexOf('.')), '')
const convert = Subtitles.convertSubText(await file.text(), extension)
if (!convert) return
const { header, type } = convert
const newtrack = this.track(trackNumber)
newtrack.styles.Default = 0
newtrack.meta = { type, header, number: '' + trackNumber, name, language: name.replace(/[,._-]/g, ' ').trim() || 'Track ' + trackNumber }
const styleMatches = header.match(stylesRx)
if (styleMatches) {
for (let i = 0; i < styleMatches.length; ++i) {
newtrack.styles[styleMatches[i]!.replace('Style:', '').trim()] = i + 1
}
}
if (this.current.value === -1) {
this.selectCaptions(trackNumber)
this.initSubtitleRenderer()
}
}
initSubtitleRenderer () {
@ -194,107 +255,78 @@ export default class Subtitles {
destroy () {
this.renderer?.destroy()
this.ctrl.abort()
this.files = []
for (const { events } of Object.values(this._tracks.value)) {
events.clear()
}
}
// async addSingleSubtitleFile (file: File) {
// // lets hope there's no more than 100 subtitle tracks in a file
// const index = 100 + this.headers.length
// this.subtitleFiles[index] = file
// const type = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()
// const subname = file.name.slice(0, file.name.lastIndexOf('.'))
// // sub name could contain video name with or without extension, possibly followed by lang, or not.
// const name = subname.includes(this.selected.name)
// ? subname.replace(this.selected.name, '')
// : subname.replace(this.selected.name.slice(0, this.selected.name.lastIndexOf('.')), '')
// this.headers[index] = {
// header: defaultHeader,
// language: name.replace(/[,._-]/g, ' ').trim() || 'Track ' + index,
// number: index,
// type
// }
// this.tracks[index] = []
// const subtitles = Subtitles.convertSubText(await file.text(), type) ?? ['']
// if (type === 'ass') {
// this.headers[index].header = subtitles
// } else {
// this.headers[index].header += subtitles.join('\n')
// }
// if (!this.current) {
// this.current = index
// this.initSubtitleRenderer()
// this.selectCaptions(this.current)
// }
// }
// static convertSubText (text: string, type: string) {
// const srtRx = /(?:\d+\r?\n)?(\S{9,12})\s?-->\s?(\S{9,12})(.*)\r?\n([\s\S]*)$/i
// const srt = (text: string) => {
// const subtitles = []
// const replaced = text.replace(/\r/g, '')
// for (const split of replaced.split(/\r?\n\r?\n/)) {
// const match: string[] | null = split.match(srtRx)
// if (match?.length !== 5) continue
// // timestamps
// match[1] = match[1]!.match(/.*[.,]\d{2}/)![0]
// match[2] = match[2]!.match(/.*[.,]\d{2}/)![0]
// if (match[1]?.length === 9) {
// match[1] = '0:' + match[1]
// } else {
// if (match[1]?.[0] === '0') {
// match[1] = match[1].substring(1)
// }
// }
// match[1]?.replace(',', '.')
// if (match[2]?.length === 9) {
// match[2] = '0:' + match[2]
// } else {
// if (match[2]?.[0] === '0') {
// match[2] = match[2].substring(1)
// }
// }
// match[2]?.replace(',', '.')
// // create array of all tags
// const matches = match[4]?.match(/<[^>]+>/g)
// if (matches) {
// matches.forEach(matched => {
// if (matched.includes('</')) { // check if its a closing tag
// match[4] = match[4]!.replace(matched, matched.replace('</', '{\\').replace('>', '0}'))
// } else {
// match[4] = match[4]!.replace(matched, matched.replace('<', '{\\').replace('>', '1}'))
// }
// })
// }
// subtitles.push('Dialogue: 0,' + match[1].replace(',', '.') + ',' + match[2].replace(',', '.') + ',Default,,0,0,0,,' + match[4]!.replace(/\r?\n/g, '\\N'))
// }
// return subtitles
// }
// const subRx = /[{[](\d+)[}\]][{[](\d+)[}\]](.+)/i
// const sub = (text: string) => {
// const subtitles = []
// const replaced = text.replace(/\r/g, '')
// let frames = 1000 / Number(replaced.match(subRx)?.[3])
// if (!frames || isNaN(frames)) frames = 41.708
// for (const split of replaced.split('\r?\n')) {
// const match = split.match(subRx)
// if (match) subtitles.push('Dialogue: 0,' + toTS((Number(match[1]) * frames) / 1000, 1) + ',' + toTS((Number(match[2]) * frames) / 1000, 1) + ',Default,,0,0,0,,' + match[3]?.replace('|', '\\N'))
// }
// return subtitles
// }
// const subtitles = type === 'ass' ? text : []
// if (type === 'ass') {
// return subtitles
// } else if (type === 'srt' || type === 'vtt') {
// return srt(text)
// } else if (type === 'sub') {
// return sub(text)
// } else {
// // subbers have a tendency to not set the extensions properly
// if (srtRx.test(text)) return srt(text)
// if (subRx.test(text)) return sub(text)
// }
// }
static convertSubText (text: string, type: string) {
const srtRx = /(?:\d+\r?\n)?(\S{9,12})\s?-->\s?(\S{9,12})(.*)\r?\n([\s\S]*)$/i
const srt = (text: string) => {
const subtitles = []
const replaced = text.replace(/\r/g, '')
for (const split of replaced.split(/\r?\n\r?\n/)) {
const match: string[] | null = split.match(srtRx)
if (match?.length !== 5) continue
// timestamps
match[1] = match[1]!.match(/.*[.,]\d{2}/)![0]
match[2] = match[2]!.match(/.*[.,]\d{2}/)![0]
if (match[1]?.length === 9) {
match[1] = '0:' + match[1]
} else {
if (match[1]?.[0] === '0') {
match[1] = match[1].substring(1)
}
}
match[1]?.replace(',', '.')
if (match[2]?.length === 9) {
match[2] = '0:' + match[2]
} else {
if (match[2]?.[0] === '0') {
match[2] = match[2].substring(1)
}
}
match[2]?.replace(',', '.')
// create array of all tags
const matches = match[4]?.match(/<[^>]+>/g)
if (matches) {
matches.forEach(matched => {
if (matched.includes('</')) { // check if its a closing tag
match[4] = match[4]!.replace(matched, matched.replace('</', '{\\').replace('>', '0}'))
} else {
match[4] = match[4]!.replace(matched, matched.replace('<', '{\\').replace('>', '1}'))
}
})
}
subtitles.push('Dialogue: 0,' + match[1].replace(',', '.') + ',' + match[2].replace(',', '.') + ',Default,,0,0,0,,' + match[4]!.replace(/\r?\n/g, '\\N'))
}
return subtitles
}
const subRx = /[{[](\d+)[}\]][{[](\d+)[}\]](.+)/i
const sub = (text: string) => {
const subtitles = []
const replaced = text.replace(/\r/g, '')
let frames = 1000 / Number(replaced.match(subRx)?.[3])
if (!frames || isNaN(frames)) frames = 41.708
for (const split of replaced.split('\r?\n')) {
const match = split.match(subRx)
if (match) subtitles.push('Dialogue: 0,' + toTS((Number(match[1]) * frames) / 1000, 1) + ',' + toTS((Number(match[2]) * frames) / 1000, 1) + ',Default,,0,0,0,,' + match[3]?.replace('|', '\\N'))
}
return subtitles
}
if (type === 'ass') {
return { type: 'ass', header: text }
} else if (type === 'srt' || type === 'vtt') {
return { type: 'srt', header: srt(text).join('\n') }
} else if (type === 'sub') {
return { type: 'sub', header: sub(text).join('\n') }
} else {
// subbers have a tendency to not set the extensions at all
if (text.startsWith('[Script Info]')) return { type: 'ass', header: text }
if (srtRx.test(text)) return { type: 'srt', header: srt(text).join('\n') }
if (subRx.test(text)) return { type: 'sub', header: sub(text).join('\n') }
}
}
}

View file

@ -53,7 +53,7 @@
<SidebarButton href='/app/settings/'>
<Settings size={18} />
</SidebarButton>
<SidebarButton href='/app/profile/' class='hidden md:block'>
<SidebarButton href='/app/profile/' class='hidden md:flex py-0'>
{#if hasAuth}
{@const viewer = client.profile()}
<Avatar.Root class='size-6 rounded-md'>

View file

@ -20,10 +20,10 @@ declare module 'gql.tada' {
TadaDocumentNode<{ MediaListCollection: { user: { id: number; } | null; lists: ({ status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; entries: ({ id: number; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; nextAiringEpisode: { episode: number; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; } | null; } | null)[] | null; } | null; } | null; } | null)[] | null; } | null)[] | null; } | null; }, { id?: number | null | undefined; }, void>;
"\n mutation CustomLists($lists: [String]) {\n UpdateUser(animeListOptions: { customLists: $lists }) {\n id\n }\n }\n":
TadaDocumentNode<{ UpdateUser: { id: number; } | null; }, { lists?: (string | null)[] | null | undefined; }, void>;
"\n fragment ScheduleMedia on Media @_unmask {\n id,\n title {\n userPreferred\n }\n mediaListEntry {\n status\n }\n aired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n },\n notaired: airingSchedule(page: 1, perPage: 50, notYetAired: true) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n }\n }\n":
TadaDocumentNode<{ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; }, {}, { fragment: "ScheduleMedia"; on: "Media"; masked: false; }>;
"\n fragment ScheduleMedia on Media @_unmask {\n id,\n title {\n userPreferred\n }\n mediaListEntry {\n status,\n id\n }\n aired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n },\n notaired: airingSchedule(page: 1, perPage: 50, notYetAired: true) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n }\n }\n":
TadaDocumentNode<{ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; }, {}, { fragment: "ScheduleMedia"; on: "Media"; masked: false; }>;
"\n query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int, $ids: [Int]) {\n curr1: Page(page: 1) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr2: Page(page: 2) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr3: Page(page: 3) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n residue: Page(page: 1) {\n media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next1: Page(page: 1) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next2: Page(page: 2) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n }\n":
TadaDocumentNode<{ curr1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr3: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; residue: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids?: (number | null)[] | null | undefined; seasonYearNext?: number | null | undefined; seasonNext?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearLast?: number | null | undefined; seasonLast?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearCurrent?: number | null | undefined; seasonCurrent?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; }, void>;
TadaDocumentNode<{ curr1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr3: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; residue: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; id: number; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids?: (number | null)[] | null | undefined; seasonYearNext?: number | null | undefined; seasonNext?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearLast?: number | null | undefined; seasonLast?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearCurrent?: number | null | undefined; seasonCurrent?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; }, void>;
"\n fragment UserFrag on User @_unmask {\n id,\n bannerImage,\n about,\n isFollowing,\n isFollower,\n donatorBadge,\n options {\n profileColor\n },\n createdAt,\n name,\n avatar {\n medium\n },\n statistics {\n anime {\n count,\n minutesWatched,\n episodesWatched,\n genres(limit: 3, sort: COUNT_DESC) {\n genre,\n count\n }\n }\n }\n }\n":
TadaDocumentNode<{ id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; }, {}, { fragment: "UserFrag"; on: "User"; masked: false; }>;
"\n query Following($id: Int!) {\n Page {\n mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {\n id,\n status,\n score,\n progress,\n user {\n ...UserFrag\n }\n }\n }\n }\n":

View file

@ -198,7 +198,8 @@ export const ScheduleMedia = gql(`
userPreferred
}
mediaListEntry {
status
status,
id
}
aired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {
n: nodes {

View file

@ -123,7 +123,7 @@ export const extensions = new class Extensions {
const deduped = this.dedupe(results)
if (!deduped.length) throw new Error('No results found.\nTry specifying a torrent manually by pasting a magnet link or torrent file into the filter bar.')
if (!deduped.length) return { results: [], errors }
const parseObjects = await anitomyscript(deduped.map(({ title }) => title))
parseObjects.forEach((parseObject, index) => {

View file

@ -257,3 +257,12 @@ export class HashMap<K extends object, T> {
return this.values()
}
}
export const subtitleExtensions = ['srt', 'vtt', 'ass', 'ssa', 'sub', 'txt']
export const subRx = new RegExp(`.(${subtitleExtensions.join('|')})$`, 'i')
export const videoExtensions = ['3g2', '3gp', 'asf', 'avi', 'dv', 'flv', 'gxf', 'm2ts', 'm4a', 'm4b', 'm4p', 'm4r', 'm4v', 'mkv', 'mov', 'mp4', 'mpd', 'mpeg', 'mpg', 'mxf', 'nut', 'ogm', 'ogv', 'swf', 'ts', 'vob', 'webm', 'wmv', 'wtv']
export const videoRx = new RegExp(`.(${videoExtensions.join('|')})$`, 'i')
export const fontExtensions = ['ttf', 'ttc', 'woff', 'woff2', 'otf', 'cff', 'otc', 'pfa', 'pfb', 'pcf', 'fnt', 'bdf', 'pfr', 'eot']
export const fontRx = new RegExp(`.(${fontExtensions.join('|')})$`, 'i')

53
src/types/events.d.ts vendored Normal file
View file

@ -0,0 +1,53 @@
declare module 'events' {
declare class EventEmitter<Events extends Record<string, unknown[]>> {
addListener<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
emit: <Event extends keyof Events>(
event: Event,
...args: Events[Event]
) => boolean
emitted<Event extends keyof Events>(
event: Event
): Promise<Events[Event]>
eventNames(): Array<keyof Events>
listeners<Event extends keyof Events>(
event: Event
): Array<(...args: Events[Event]) => unknown>
listenerCount(event: keyof Events): number
on<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
once<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
prependListener<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
prependOnceListener<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
removeAllListeners(event?: keyof Events): this
removeListener<Event extends keyof Events>(
event: Event,
listener: (...args: Events[Event]) => unknown
): this
setMaxListeners(n: number): this
getMaxListeners(): number
}
export { EventEmitter as default, EventEmitter }
}

View file

@ -22,7 +22,10 @@
"tadaTurboLocation": "./src/lib/modules/anilist/graphql-turbo.d.ts"
}
],
"maxNodeModuleJsDepth": 3
"maxNodeModuleJsDepth": 3,
"typeRoots": [
"./src/types"
],
},
"typeAcquisition": {
"enable": false