mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 02:21:49 +00:00
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:
parent
d2335b1e15
commit
b76ce678df
15 changed files with 235 additions and 138 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
1
src/app.d.ts
vendored
|
|
@ -139,7 +139,6 @@ declare global {
|
|||
declare module '*.svelte' {
|
||||
export default SvelteComponentTyped
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
declare module '*.svelte' {
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
6
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
6
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -198,7 +198,8 @@ export const ScheduleMedia = gql(`
|
|||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
status,
|
||||
id
|
||||
}
|
||||
aired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {
|
||||
n: nodes {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
53
src/types/events.d.ts
vendored
Normal 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 }
|
||||
}
|
||||
|
|
@ -22,7 +22,10 @@
|
|||
"tadaTurboLocation": "./src/lib/modules/anilist/graphql-turbo.d.ts"
|
||||
}
|
||||
],
|
||||
"maxNodeModuleJsDepth": 3
|
||||
"maxNodeModuleJsDepth": 3,
|
||||
"typeRoots": [
|
||||
"./src/types"
|
||||
],
|
||||
},
|
||||
"typeAcquisition": {
|
||||
"enable": false
|
||||
|
|
|
|||
Loading…
Reference in a new issue