mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 21:12:10 +00:00
fix: improve episode search, more date info, dont query batches or movies when not necessary
This commit is contained in:
parent
fabff3a32c
commit
ce9e01c2c2
10 changed files with 236 additions and 204 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "6.4.49",
|
||||
"version": "6.4.50",
|
||||
"license": "BUSL-1.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.5",
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"sync": "svelte-kit sync",
|
||||
"check": "svelte-check --threshold error --tsconfig ./tsconfig.web.json",
|
||||
"check:watch": "svelte-check -threshold error --tsconfig ./tsconfig.web.json --watch",
|
||||
"test": "pnpm run sync && pnpm run lint && pnpm run gql:check && pnpm run check",
|
||||
"lint": "eslint --quiet -c eslint.config.js",
|
||||
"lint:fix": "eslint --quiet -c eslint.config.js --fix",
|
||||
"gql:turbo": "node ./node_modules/gql.tada/bin/cli.js turbo -c ./tsconfig.web.json",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
<script context='module' lang='ts'>
|
||||
export let fillerEpisodes: Record<number, number[] | undefined> = {}
|
||||
|
||||
fetch('https://raw.githubusercontent.com/ThaUnknown/filler-scrape/master/filler.json').then(async res => {
|
||||
fillerEpisodes = await res.json()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang='ts'>
|
||||
import ChevronLeft from 'lucide-svelte/icons/chevron-left'
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right'
|
||||
|
|
@ -19,8 +11,9 @@
|
|||
|
||||
import type { EpisodesResponse } from '$lib/modules/anizip/types'
|
||||
|
||||
import { episodes as _episodes, dedupeAiring, episodeByAirDate, notes, type Media } from '$lib/modules/anilist'
|
||||
import { episodes as _episodes, dedupeAiring, notes, type Media } from '$lib/modules/anilist'
|
||||
import { authAggregator, list, progress } from '$lib/modules/auth'
|
||||
import { makeEpisodeList } from '$lib/modules/extensions'
|
||||
import { click, dragScroll } from '$lib/modules/navigate'
|
||||
import { liveAnimeProgress } from '$lib/modules/watchProgress'
|
||||
import { breakpoints, cn, since } from '$lib/utils'
|
||||
|
|
@ -28,9 +21,7 @@
|
|||
export let eps: EpisodesResponse | null
|
||||
export let media: Media
|
||||
|
||||
$: episodeCount = Math.max(_episodes(media) ?? 0, eps?.episodeCount ?? 0)
|
||||
|
||||
$: ({ episodes, specialCount } = eps ?? {})
|
||||
$: episodeCount = _episodes(media) ?? eps?.episodeCount ?? 0
|
||||
|
||||
const alSchedule: Record<number, Date | undefined> = {}
|
||||
|
||||
|
|
@ -40,22 +31,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: episodeList = media && Array.from({ length: episodeCount }, (_, i) => {
|
||||
const episode = i + 1
|
||||
|
||||
const airingAt = alSchedule[episode]
|
||||
// TODO handle special cases where anilist reports that 3 episodes aired at the same time because of pre-releases, simply don't allow the same episode to be re-used
|
||||
|
||||
const hasSpecial = !!specialCount
|
||||
const hasEpisode = episodes?.[Number(episode)]
|
||||
const hasCountMatch = (_episodes(media) ?? 0) === (eps?.episodeCount ?? 0)
|
||||
|
||||
const needsValidation = !(!hasSpecial || (hasEpisode && hasCountMatch))
|
||||
const { image, summary, overview, rating, title, length, airdate } = (needsValidation ? episodeByAirDate(airingAt, episodes ?? {}, episode) : episodes?.[Number(episode)]) ?? {}
|
||||
return {
|
||||
episode, image, summary: summary ?? overview, rating, title, length, airdate, airingAt, filler: !!fillerEpisodes[media.id]?.includes(i + 1)
|
||||
}
|
||||
})
|
||||
$: episodeList = media && makeEpisodeList(episodeCount, media, eps)
|
||||
|
||||
const perPage = 16
|
||||
|
||||
|
|
|
|||
|
|
@ -64,13 +64,13 @@
|
|||
import { saved } from '$lib/modules/extensions'
|
||||
import { server } from '$lib/modules/torrent'
|
||||
|
||||
$: open = !!$searchStore.media
|
||||
$: open = !!$searchStore?.media
|
||||
|
||||
$: searchResult = !!$searchStore.media && extensions.getResultsFromExtensions({ media: $searchStore.media, episode: $searchStore.episode, resolution: $settings.searchQuality })
|
||||
$: searchResult = !!$searchStore?.media && extensions.getResultsFromExtensions({ media: $searchStore.media, episode: $searchStore.episode, resolution: $settings.searchQuality })
|
||||
|
||||
function close (state = false) {
|
||||
if (!state) {
|
||||
searchStore.set({})
|
||||
searchStore.set(undefined)
|
||||
open = false
|
||||
inputText = ''
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
let inputText = ''
|
||||
|
||||
function play (result: Pick<TorrentResult, 'hash'>) {
|
||||
server.play(result.hash, $searchStore.media!, $searchStore.episode!)
|
||||
server.play(result.hash, $searchStore!.media, $searchStore!.episode)
|
||||
goto('/app/player/')
|
||||
close()
|
||||
}
|
||||
|
|
@ -163,153 +163,153 @@
|
|||
|
||||
<Dialog.Root bind:open onOpenChange={close} portal='#episodeListTarget'>
|
||||
<Dialog.Content class='bg-black h-full lg:border-x-4 border-b-0 max-w-5xl w-full max-h-[calc(100%-1rem)] mt-2 p-0 items-center flex lg:rounded-t-xl overflow-hidden z-[100]'>
|
||||
<div class='absolute top-0 left-0 w-full h-full max-h-28 overflow-hidden'>
|
||||
{#if $searchStore.media}
|
||||
{#if $searchStore}
|
||||
<div class='absolute top-0 left-0 w-full h-full max-h-28 overflow-hidden'>
|
||||
<Banner media={$searchStore.media} class='object-cover w-full h-full absolute bottom-[0.5px] left-0 -z-10' />
|
||||
{/if}
|
||||
<div class='w-full h-full banner-2' />
|
||||
</div>
|
||||
<div class='gap-4 w-full relative h-full flex flex-col pt-6'>
|
||||
<div class='px-4 sm:px-6 space-y-4'>
|
||||
<div class='font-weight-bold text-2xl font-bold text-ellipsis text-nowrap overflow-hidden pb-2'>{$searchStore.media ? title($searchStore.media) : ''}</div>
|
||||
<div class='flex items-center relative scale-parent'>
|
||||
<Input
|
||||
autofocus={false}
|
||||
class='pl-9 bg-background select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50'
|
||||
placeholder='Filter by text, or paste a magnet link or torrent file to specify a torrent manually'
|
||||
bind:value={inputText} />
|
||||
<MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' />
|
||||
</div>
|
||||
<div class='flex items-center gap-4 justify-around flex-wrap'>
|
||||
<div class='flex items-center space-x-2 grow'>
|
||||
<span>Episode</span>
|
||||
<Input type='number' inputmode='numeric' pattern='[0-9]*' min='0' max='65536' bind:value={$searchStore.episode} class='w-32 shrink-0 bg-background grow' />
|
||||
</div>
|
||||
<div class='flex items-center space-x-2 grow'>
|
||||
<span>Resolution</span>
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={videoResolutions} portal='#episodeListTarget' class='w-32 shrink-0 grow border-border border' />
|
||||
</div>
|
||||
</div>
|
||||
<ProgressButton
|
||||
onclick={playBest}
|
||||
size='default'
|
||||
class='w-full font-bold'
|
||||
bind:animating>
|
||||
Auto Select Torrent
|
||||
</ProgressButton>
|
||||
<div class='w-full h-full banner-2' />
|
||||
</div>
|
||||
<div class='h-full overflow-y-auto px-4 sm:px-6 pt-2' role='menu' tabindex='-1' on:keydown={stopAnimation} on:pointerenter={stopAnimation} on:pointermove={stopAnimation} use:dragScroll>
|
||||
{#await Promise.all([searchResult, $downloaded])}
|
||||
{#each Array.from({ length: 12 }) as _, i (i)}
|
||||
<div class='p-3 h-[104px] flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border flex-col justify-between'>
|
||||
<div class='h-4 w-40 bg-primary/5 animate-pulse rounded mt-2' />
|
||||
<div class='bg-primary/5 animate-pulse rounded h-2 w-28 mt-1' />
|
||||
<div class='flex justify-between mb-1'>
|
||||
<div class='flex gap-2'>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
<div class='gap-4 w-full relative h-full flex flex-col pt-6'>
|
||||
<div class='px-4 sm:px-6 space-y-4'>
|
||||
<div class='font-weight-bold text-2xl font-bold text-ellipsis text-nowrap overflow-hidden pb-2'>{title($searchStore.media) ?? ''}</div>
|
||||
<div class='flex items-center relative scale-parent'>
|
||||
<Input
|
||||
autofocus={false}
|
||||
class='pl-9 bg-background select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50'
|
||||
placeholder='Filter by text, or paste a magnet link or torrent file to specify a torrent manually'
|
||||
bind:value={inputText} />
|
||||
<MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' />
|
||||
</div>
|
||||
<div class='flex items-center gap-4 justify-around flex-wrap'>
|
||||
<div class='flex items-center space-x-2 grow'>
|
||||
<span>Episode</span>
|
||||
<Input type='number' inputmode='numeric' pattern='[0-9]*' min='0' max='65536' bind:value={$searchStore.episode} class='w-32 shrink-0 bg-background grow' />
|
||||
</div>
|
||||
<div class='flex items-center space-x-2 grow'>
|
||||
<span>Resolution</span>
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={videoResolutions} portal='#episodeListTarget' class='w-32 shrink-0 grow border-border border' />
|
||||
</div>
|
||||
</div>
|
||||
<ProgressButton
|
||||
onclick={playBest}
|
||||
size='default'
|
||||
class='w-full font-bold'
|
||||
bind:animating>
|
||||
Auto Select Torrent
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<div class='h-full overflow-y-auto px-4 sm:px-6 pt-2' role='menu' tabindex='-1' on:keydown={stopAnimation} on:pointerenter={stopAnimation} on:pointermove={stopAnimation} use:dragScroll>
|
||||
{#await Promise.all([searchResult, $downloaded])}
|
||||
{#each Array.from({ length: 12 }) as _, i (i)}
|
||||
<div class='p-3 h-[104px] flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border flex-col justify-between'>
|
||||
<div class='h-4 w-40 bg-primary/5 animate-pulse rounded mt-2' />
|
||||
<div class='bg-primary/5 animate-pulse rounded h-2 w-28 mt-1' />
|
||||
<div class='flex justify-between mb-1'>
|
||||
<div class='flex gap-2'>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
</div>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
</div>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:then [search, downloaded]}
|
||||
{@const media = $searchStore.media}
|
||||
{#if search && media}
|
||||
{@const { results, errors } = search}
|
||||
{#each filterAndSortResults(results, inputText, downloaded) as result (result.hash)}
|
||||
<div class='p-3 flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border select:ring-1 select:ring-ring select:bg-accent select:text-accent-foreground select:scale-[1.02] select:shadow-lg scale-100 transition-all' class:opacity-40={result.accuracy === 'low'} use:click={() => play(result)} title={result.parseObject.file_name[0]}>
|
||||
{#if result.accuracy === 'high'}
|
||||
<div class='absolute top-0 left-0 w-full h-full -z-10'>
|
||||
<Banner {media} class='object-cover w-full h-full' />
|
||||
<div class='absolute top-0 left-0 w-full h-full banner' />
|
||||
</div>
|
||||
{/if}
|
||||
<div class='flex pl-2 flex-col justify-between w-full h-20 relative min-w-0 text-[.7rem]'>
|
||||
<div class='flex w-full items-center'>
|
||||
{#if downloaded.has(result.hash)}
|
||||
<Download class='mr-2 text-[#53da33]' size='1.2rem' />
|
||||
{:else if result.type === 'batch'}
|
||||
<Database class='mr-2' size='1.2rem' />
|
||||
{:else if result.accuracy === 'high'}
|
||||
<BadgeCheck class='mr-2 text-[#53da33]' size='1.2rem' />
|
||||
{/if}
|
||||
<div class='text-xl font-bold text-nowrap'>{result.parseObject.release_group[0] && result.parseObject.release_group[0].length < 20 ? result.parseObject.release_group[0] : 'No Group'}</div>
|
||||
<div class='ml-auto flex gap-2 self-start'>
|
||||
{#each result.extension as id (id)}
|
||||
{#if $saved[id]}
|
||||
<img src={$saved[id].icon} alt={id} class='size-4' title='Provided by {id}' decoding='async' loading='lazy' />
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{:then [search, downloaded]}
|
||||
{@const media = $searchStore.media}
|
||||
{#if search && media}
|
||||
{@const { results, errors } = search}
|
||||
{#each filterAndSortResults(results, inputText, downloaded) as result (result.hash)}
|
||||
<div class='p-3 flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border select:ring-1 select:ring-ring select:bg-accent select:text-accent-foreground select:scale-[1.02] select:shadow-lg scale-100 transition-all' class:opacity-40={result.accuracy === 'low'} use:click={() => play(result)} title={result.parseObject.file_name[0]}>
|
||||
{#if result.accuracy === 'high'}
|
||||
<div class='absolute top-0 left-0 w-full h-full -z-10'>
|
||||
<Banner {media} class='object-cover w-full h-full' />
|
||||
<div class='absolute top-0 left-0 w-full h-full banner' />
|
||||
</div>
|
||||
</div>
|
||||
<div class='text-muted-foreground text-ellipsis text-nowrap overflow-hidden'>{simplifyFilename(result.parseObject)}</div>
|
||||
<div class='flex flex-row leading-none'>
|
||||
<div class='details text-light flex'>
|
||||
<span class='text-nowrap flex items-center'>{fastPrettyBytes(result.size)}</span>
|
||||
<span class='text-nowrap flex items-center'>{result.seeders} Seeders</span>
|
||||
<span class='text-nowrap flex items-center'>{since(new Date(result.date))}</span>
|
||||
</div>
|
||||
<div class='flex ml-auto flex-row-reverse'>
|
||||
{#if result.type === 'best'}
|
||||
<div class='rounded px-3 py-1 ml-2 border text-nowrap flex items-center' style='background: #1d2d1e; border-color: #53da33 !important; color: #53da33'>
|
||||
Best Release
|
||||
</div>
|
||||
{:else if result.type === 'alt'}
|
||||
<div class='rounded px-3 py-1 ml-2 border text-nowrap flex items-center' style='background: #391d20; border-color: #c52d2d !important; color: #c52d2d'>
|
||||
Alt Release
|
||||
</div>
|
||||
{/if}
|
||||
<div class='flex pl-2 flex-col justify-between w-full h-20 relative min-w-0 text-[.7rem]'>
|
||||
<div class='flex w-full items-center'>
|
||||
{#if downloaded.has(result.hash)}
|
||||
<Download class='mr-2 text-[#53da33]' size='1.2rem' />
|
||||
{:else if result.type === 'batch'}
|
||||
<Database class='mr-2' size='1.2rem' />
|
||||
{:else if result.accuracy === 'high'}
|
||||
<BadgeCheck class='mr-2 text-[#53da33]' size='1.2rem' />
|
||||
{/if}
|
||||
{#each sanitiseTerms(result.parseObject) as { text, color }, i (i)}
|
||||
<div class='rounded px-3 py-1 ml-2 text-nowrap font-bold flex items-center' style:background={color}>
|
||||
<div class='text-contrast'>
|
||||
{text}
|
||||
<div class='text-xl font-bold text-nowrap'>{result.parseObject.release_group[0] && result.parseObject.release_group[0].length < 20 ? result.parseObject.release_group[0] : 'No Group'}</div>
|
||||
<div class='ml-auto flex gap-2 self-start'>
|
||||
{#each result.extension as id (id)}
|
||||
{#if $saved[id]}
|
||||
<img src={$saved[id].icon} alt={id} class='size-4' title='Provided by {id}' decoding='async' loading='lazy' />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class='text-muted-foreground text-ellipsis text-nowrap overflow-hidden'>{simplifyFilename(result.parseObject)}</div>
|
||||
<div class='flex flex-row leading-none'>
|
||||
<div class='details text-light flex'>
|
||||
<span class='text-nowrap flex items-center'>{fastPrettyBytes(result.size)}</span>
|
||||
<span class='text-nowrap flex items-center'>{result.seeders} Seeders</span>
|
||||
<span class='text-nowrap flex items-center'>{since(new Date(result.date))}</span>
|
||||
</div>
|
||||
<div class='flex ml-auto flex-row-reverse'>
|
||||
{#if result.type === 'best'}
|
||||
<div class='rounded px-3 py-1 ml-2 border text-nowrap flex items-center' style='background: #1d2d1e; border-color: #53da33 !important; color: #53da33'>
|
||||
Best Release
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if result.type === 'alt'}
|
||||
<div class='rounded px-3 py-1 ml-2 border text-nowrap flex items-center' style='background: #391d20; border-color: #c52d2d !important; color: #c52d2d'>
|
||||
Alt Release
|
||||
</div>
|
||||
{/if}
|
||||
{#each sanitiseTerms(result.parseObject) as { text, color }, i (i)}
|
||||
<div class='rounded px-3 py-1 ml-2 text-nowrap font-bold flex items-center' style:background={color}>
|
||||
<div class='text-contrast'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</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.
|
||||
{: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>
|
||||
</div>
|
||||
{/each}
|
||||
{#each errors as error, i (i)}
|
||||
<div class='p-5 flex items-center justify-center w-full h-80'>
|
||||
<div>
|
||||
<div class='mb-1 font-bold text-2xl text-center '>
|
||||
Extensions {error.extension} encountered an error
|
||||
</div>
|
||||
<div class='text-md text-center text-muted-foreground whitespace-pre-wrap'>
|
||||
{error.error.stack}
|
||||
{/each}
|
||||
{#each errors as error, i (i)}
|
||||
<div class='p-5 flex items-center justify-center w-full h-80'>
|
||||
<div>
|
||||
<div class='mb-1 font-bold text-2xl text-center '>
|
||||
Extensions {error.extension} encountered an error
|
||||
</div>
|
||||
<div class='text-md text-center text-muted-foreground whitespace-pre-wrap'>
|
||||
{error.error.stack}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{:catch error}
|
||||
<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 whitespace-pre-wrap'>
|
||||
{error.message}
|
||||
{/each}
|
||||
{/if}
|
||||
{:catch error}
|
||||
<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 whitespace-pre-wrap'>
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
import type { resolveFilesPoorly, ResolvedFile } from './resolver'
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
import { fillerEpisodes } from '$lib/components/EpisodesList.svelte'
|
||||
import { cover, episodes, title, type Media } from '$lib/modules/anilist'
|
||||
import { fillerEpisodes } from '$lib/modules/extensions'
|
||||
import { settings } from '$lib/modules/settings'
|
||||
import { server } from '$lib/modules/torrent'
|
||||
import { w2globby } from '$lib/modules/w2g/lobby'
|
||||
|
|
|
|||
|
|
@ -230,8 +230,6 @@ const AnimeResolver = new class AnimeResolver {
|
|||
if (!fileName.length) return []
|
||||
const parseObjs = await anitomyscript(fileName)
|
||||
|
||||
const TYPE_EXCLUSIONS = ['ED', 'ENDING', 'NCED', 'NCOP', 'OP', 'OPENING', 'PREVIEW', 'PV']
|
||||
|
||||
const uniq: Record<string, AnitomyResult> = {}
|
||||
for (const obj of parseObjs) {
|
||||
const key = this.getCacheKeyForTitle(obj)
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ export const DEFAULT_EXTENSIONS = 'gh:hayase-app/extensions'
|
|||
export const SETUP_VERSION = 3
|
||||
|
||||
// episode is optional here, but is actually always defined
|
||||
export const searchStore = writable<{episode?: number, media?: Media}>({})
|
||||
export const searchStore = writable<{episode: number, media: Media} | undefined>(undefined)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ScheduleMedia } from './queries'
|
||||
import type { Media } from './types'
|
||||
import type { Media, MediaEdge } from './types'
|
||||
import type { Episode, Episodes } from '../anizip/types'
|
||||
import type { ResultOf } from 'gql.tada'
|
||||
|
||||
|
|
@ -34,6 +34,17 @@ export function title (media: Pick<Media, 'title'>): string {
|
|||
return media.title?.userPreferred ?? 'TBA'
|
||||
}
|
||||
|
||||
export function getParentForSpecial (media: Media) {
|
||||
if (!['SPECIAL', 'OVA', 'ONA'].some(format => media.format === format)) return false
|
||||
const animeRelations = (media.relations?.edges?.filter(edge => edge?.node?.type === 'ANIME') ?? []) as MediaEdge[]
|
||||
|
||||
return getRelation(animeRelations, 'PARENT') ?? getRelation(animeRelations, 'PREQUEL') ?? getRelation(animeRelations, 'SEQUEL')
|
||||
}
|
||||
|
||||
export function getRelation (list: MediaEdge[], type: MediaEdge['relationType']) {
|
||||
return list.find(edge => edge.relationType === type)?.node?.id
|
||||
}
|
||||
|
||||
const STATUS_MAP = {
|
||||
RELEASING: 'Releasing',
|
||||
NOT_YET_RELEASED: 'Not Yet Released',
|
||||
|
|
@ -122,6 +133,10 @@ export function isMovie (media: Pick<Media, 'format' | 'title' | 'synonyms' | 'd
|
|||
return (media.duration ?? 0) > 80 && media.episodes === 1
|
||||
}
|
||||
|
||||
export function isSingleEpisode (media: Pick<Media, 'format' | 'title' | 'synonyms' | 'duration' | 'episodes'>) {
|
||||
return media.episodes === 1 || (isMovie(media) && !media.episodes)
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
export const currentSeason = ['WINTER', 'SPRING', 'SUMMER', 'FALL'][Math.floor((date.getMonth() / 12) * 4) % 4] as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
|
||||
export const currentYear = date.getFullYear()
|
||||
|
|
|
|||
|
|
@ -433,13 +433,14 @@ export default new class KitsuSync {
|
|||
following (id: number) {
|
||||
return null
|
||||
// TODO: this doesnt work
|
||||
// const viewer = get(this.viewer)
|
||||
// this._get<Res<KEntry, Anime | Mapping>>(
|
||||
// ENDPOINTS.API_USER_LIBRARY,
|
||||
// {
|
||||
// 'filter[following]': true,
|
||||
// 'filter[user_id]': this.viewer.value?.id,
|
||||
// 'filter[animeId]': 42765,
|
||||
// include: 'anime.mappings,user'
|
||||
// 'filter[following]': viewer?.id,
|
||||
// // 'filter[user_id]': viewer?.id,
|
||||
// 'filter[animeId]': 42765
|
||||
// // include: 'anime.mappings,user'
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import anitomyscript, { type AnitomyResult } from 'anitomyscript'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { dedupeAiring, episodeByAirDate, episodes, isMovie, type Media, type MediaEdge } from '../anilist'
|
||||
import { dedupeAiring, episodeByAirDate, episodes, isMovie, type Media, getParentForSpecial, isSingleEpisode } from '../anilist'
|
||||
import { episodes as _episodes } from '../anizip'
|
||||
import native from '../native'
|
||||
import { settings, type videoResolutions } from '../settings'
|
||||
|
||||
import { storage } from './storage'
|
||||
|
||||
import type { EpisodesResponse } from '../anizip/types'
|
||||
import type { EpisodesResponse, Titles } from '../anizip/types'
|
||||
import type { TorrentResult } from 'hayase-extensions'
|
||||
|
||||
import { dev } from '$app/environment'
|
||||
|
|
@ -32,6 +32,61 @@ video.remove()
|
|||
|
||||
const debug = console.log
|
||||
|
||||
export let fillerEpisodes: Record<number, number[] | undefined> = {}
|
||||
|
||||
fetch('https://raw.githubusercontent.com/ThaUnknown/filler-scrape/master/filler.json').then(async res => {
|
||||
fillerEpisodes = await res.json()
|
||||
})
|
||||
|
||||
// TODO: these 2 exports need to be moved to a better place
|
||||
export interface SingleEpisode {
|
||||
episode: number
|
||||
image?: string
|
||||
summary?: string
|
||||
rating?: string
|
||||
title?: Titles
|
||||
length?: number
|
||||
airdate?: string
|
||||
airingAt?: Date
|
||||
filler: boolean
|
||||
anidbEid?: number
|
||||
}
|
||||
|
||||
// TODO: https://anilist.co/anime/13055/
|
||||
export function makeEpisodeList (count: number, media: Media, episodesRes?: EpisodesResponse | null) {
|
||||
const alSchedule: Record<number, Date | undefined> = {}
|
||||
|
||||
for (const { a: airingAt, e: episode } of dedupeAiring(media)) {
|
||||
alSchedule[episode] = new Date(airingAt * 1000)
|
||||
}
|
||||
|
||||
if (!alSchedule[1] && isSingleEpisode(media) && media.startDate) {
|
||||
alSchedule[1] = new Date(media.startDate.year ?? 0, (media.startDate.month ?? 1) - 1, media.startDate.day ?? 1)
|
||||
}
|
||||
|
||||
const episodeList: SingleEpisode[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const episode = i + 1
|
||||
|
||||
const airingAt = alSchedule[episode]
|
||||
|
||||
const hasSpecial = !!episodesRes?.specialCount
|
||||
const hasEpisode = episodesRes?.episodes?.[Number(episode)]
|
||||
const hasCountMatch = (episodes(media) ?? 0) === (episodesRes?.episodeCount ?? 0)
|
||||
|
||||
const needsValidation = !(!hasSpecial || (hasEpisode && hasCountMatch))
|
||||
// handle special cases where anilist reports that 3 episodes aired at the same time because of pre-releases, simply don't allow the same episode to be re-used
|
||||
const filtered = Object.fromEntries(Object.entries(episodesRes?.episodes ?? {}).filter(([_, ep]) => !episodeList.some(e => e.anidbEid === ep.anidbEid && ep.anidbEid != null)))
|
||||
|
||||
const { image, summary, overview, rating, title, length, airdate, anidbEid } = (needsValidation ? episodeByAirDate(airingAt, filtered, episode) : episodesRes?.episodes?.[Number(episode)]) ?? {}
|
||||
const res = {
|
||||
episode, image, summary: summary ?? overview, rating, title, length, airdate, airingAt, filler: !!fillerEpisodes[media.id]?.includes(i + 1), anidbEid
|
||||
}
|
||||
episodeList.push(res)
|
||||
}
|
||||
return episodeList
|
||||
}
|
||||
|
||||
export const extensions = new class Extensions {
|
||||
// this is for the most part useless, but some extensions might need it
|
||||
createTitles (media: Media) {
|
||||
|
|
@ -64,7 +119,7 @@ export const extensions = new class Extensions {
|
|||
return titles
|
||||
}
|
||||
|
||||
async getResultsFromExtensions ({ media, episode, resolution }: { media: Media, episode?: number, resolution: keyof typeof videoResolutions }) {
|
||||
async getResultsFromExtensions ({ media, episode, resolution }: { media: Media, episode: number, resolution: keyof typeof videoResolutions }) {
|
||||
await storage.modules
|
||||
const workers = storage.workers
|
||||
if (!Object.values(workers).length) {
|
||||
|
|
@ -73,6 +128,7 @@ export const extensions = new class Extensions {
|
|||
}
|
||||
|
||||
const movie = isMovie(media)
|
||||
const singleEp = isSingleEpisode(media)
|
||||
|
||||
debug(`Fetching sources for ${media.id}:${media.title?.userPreferred} ${episode} ${movie} ${resolution}`)
|
||||
|
||||
|
|
@ -104,7 +160,7 @@ export const extensions = new class Extensions {
|
|||
try {
|
||||
const promises: Array<Promise<TorrentResult[]>> = []
|
||||
promises.push(worker.single(options))
|
||||
promises.push(movie ? worker.movie(options) : worker.batch(options))
|
||||
if (!singleEp && (movie || media.status === 'FINISHED')) promises.push(movie ? worker.movie(options) : worker.batch(options))
|
||||
|
||||
for (const result of await Promise.allSettled(promises)) {
|
||||
if (result.status === 'fulfilled') {
|
||||
|
|
@ -167,39 +223,14 @@ export const extensions = new class Extensions {
|
|||
const json = await _episodes(media.id)
|
||||
if (json?.mappings?.anidb_id) return json
|
||||
|
||||
const parentID = this.getParentForSpecial(media)
|
||||
const parentID = getParentForSpecial(media)
|
||||
if (!parentID) return
|
||||
|
||||
return await _episodes(parentID)
|
||||
}
|
||||
|
||||
getParentForSpecial (media: Media) {
|
||||
if (!['SPECIAL', 'OVA', 'ONA'].some(format => media.format === format)) return false
|
||||
const animeRelations = (media.relations?.edges?.filter(edge => edge?.node?.type === 'ANIME') ?? []) as MediaEdge[]
|
||||
|
||||
return this.getRelation(animeRelations, 'PARENT') ?? this.getRelation(animeRelations, 'PREQUEL') ?? this.getRelation(animeRelations, 'SEQUEL')
|
||||
}
|
||||
|
||||
getRelation (list: MediaEdge[], type: MediaEdge['relationType']) {
|
||||
return list.find(edge => edge.relationType === type)?.node?.id
|
||||
}
|
||||
|
||||
// TODO: https://anilist.co/anime/13055/
|
||||
async ALtoAniDBEpisode ({ media, episode }: {media: Media, episode?: number}, { episodes, episodeCount, specialCount }: EpisodesResponse) {
|
||||
debug(`Fetching AniDB episode for ${media.id}:${media.title?.userPreferred} ${episode}`)
|
||||
if (!episode || !Object.values(episodes!).length) return
|
||||
// if media has no specials or their episode counts don't match
|
||||
if (!specialCount || (media.episodes && media.episodes === episodeCount && (Number(episode) in episodes!))) {
|
||||
debug('No specials found, or episode count matches between AL and AniDB')
|
||||
return episodes![Number(episode)]
|
||||
}
|
||||
debug(`Episode count mismatch between AL and AniDB for ${media.id}:${media.title?.userPreferred}`)
|
||||
const date = dedupeAiring(media).find(({ e }) => e === episode)?.a
|
||||
// TODO: if media only has one episode, and airdate doesn't exist use start/release/end dates
|
||||
const alDate = date ? new Date(date * 1000) : undefined
|
||||
debug(`AL Airdate: ${alDate?.toString()}`)
|
||||
|
||||
return episodeByAirDate(alDate, episodes!, episode)
|
||||
async ALtoAniDBEpisode ({ media, episode }: {media: Media, episode: number}, episodesRes: EpisodesResponse) {
|
||||
return makeEpisodeList(Math.max(episodes(media) ?? 0, episodesRes.episodeCount ?? 0), media, episodesRes)[episode - 1] ?? undefined
|
||||
}
|
||||
|
||||
dedupe <T extends TorrentResult & { extension: Set<string> }> (entries: T[]): T[] {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { get } from 'svelte/store'
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
import { getParentForSpecial } from '$lib/modules/anilist'
|
||||
import { asyncStore } from '$lib/modules/anilist/client'
|
||||
import { IDMedia } from '$lib/modules/anilist/queries'
|
||||
import { episodes } from '$lib/modules/anizip'
|
||||
|
|
@ -7,7 +10,14 @@ import { episodes } from '$lib/modules/anizip'
|
|||
export const load: LayoutLoad = async ({ params, fetch }) => {
|
||||
const store = asyncStore(IDMedia, { id: Number(params.id) }, { requestPolicy: 'cache-first' })
|
||||
|
||||
const eps = await episodes(Number(params.id), fetch)
|
||||
let eps = await episodes(Number(params.id), fetch)
|
||||
|
||||
if (!eps?.mappings?.anidb_id) {
|
||||
const anime = await store
|
||||
const parentID = getParentForSpecial(get(anime).Media!)
|
||||
if (!parentID) return { eps, anime }
|
||||
eps = await episodes(parentID, fetch)
|
||||
}
|
||||
|
||||
return {
|
||||
eps,
|
||||
|
|
|
|||
Loading…
Reference in a new issue