feat: tosho quality search

fix: player load stuck
wip: search
This commit is contained in:
ThaUnknown 2023-06-28 22:15:30 +02:00
parent 98beb4e8e4
commit 851281b2f3
11 changed files with 130 additions and 86 deletions

View file

@ -25,8 +25,6 @@
setContext('view', view)
setContext('gallery', writable(null))
setContext('trailer', writable(null))
</script>

View file

@ -1,13 +1,12 @@
<script>
import { getContext } from 'svelte'
import Home from './views/Home/Home.svelte'
import MediaHandler from './views/Player/MediaHandler.svelte'
import Settings from './views/Settings.svelte'
import WatchTogether from './views/WatchTogether/WatchTogether.svelte'
import Miniplayer from 'svelte-miniplayer'
import Search from './views/Search.svelte'
export let page = 'home'
const current = getContext('gallery')
</script>
<Miniplayer active={page !== 'player'} class='bg-dark-light z-10 {page === 'player' ? 'h-full' : ''}' minwidth='35rem' maxwidth='45rem' width='300px' padding='2rem'>
@ -16,7 +15,9 @@
{#if page === 'settings'}
<Settings />
{:else if page === 'home'}
<Home bind:current={$current} />
<Home />
{:else if page === 'search'}
<Search />
{:else if page === 'watchtogether'}
<WatchTogether />
{/if}

View file

@ -4,9 +4,7 @@
export let page
const view = getContext('view')
const trailer = getContext('trailer')
const gallery = getContext('gallery')
function close () {
$gallery = null
$view = null
$trailer = null
page = 'home'

View file

@ -1,17 +1,19 @@
<script context='module'>
const badgeKeys = ['search', 'genre', 'season', 'year', 'format', 'status', 'sort']
export function searchCleanup (search) {
return Object.entries(search).map(([key, value]) => {
return badgeKeys.includes(key) && value
}).filter(a => a)
}
</script>
<script>
import { traceAnime } from '@/modules/anime.js'
export let search
export let current
export let media = null
export let loadCurrent
let searchTimeout = null
export let search = {}
let searchTextInput
function searchCleanup (search) {
return Object.values(search).filter(a => a)
}
$: sanitisedSearch = searchCleanup(search)
function searchClear () {
@ -24,28 +26,9 @@
status: '',
sort: ''
}
current = null
searchTextInput?.focus()
}
function input () {
if (!searchTimeout) {
if (Object.values(search).filter(v => v).length) media = [new Promise(() => {})]
} else {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
if (current === null) {
if (Object.values(search).filter(v => v).length) current = 'search'
} else {
if (Object.values(search).filter(v => v).length) {
loadCurrent(false)
} else {
current = null
}
}
searchTimeout = null
}, 500)
}
function handleFile ({ target }) {
const { files } = target
if (files?.[0]) {
@ -55,7 +38,7 @@
}
</script>
<div class='container-fluid py-20 px-10 pb-0 position-sticky top-0 search-container z-40 bg-dark' on:input={input}>
<div class='container-fluid py-20 px-10 pb-0 position-sticky top-0 search-container z-40 bg-dark'>
<div class='row'>
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
@ -71,7 +54,6 @@
on:input={({ target }) => {
queueMicrotask(() => {
search.search = target.value
input()
})
}}
bind:this={searchTextInput}
@ -81,6 +63,7 @@
autocomplete='off'
bind:value={search.search}
data-option='search'
disabled={search.disableSearch}
placeholder='Any' />
</div>
</div>
@ -90,7 +73,7 @@
Genre
</div>
<div class='input-group'>
<select class='form-control bg-dark-light' required bind:value={search.genre}>
<select class='form-control bg-dark-light' required bind:value={search.genre} disabled={search.disableSearch}>
<option value selected disabled hidden>Any</option>
<option value='Action'>Action</option>
<option value='Adventure'>Adventure</option>
@ -119,7 +102,7 @@
Season
</div>
<div class='input-group'>
<select class='form-control bg-dark-light border-right-dark' required bind:value={search.season}>
<select class='form-control bg-dark-light border-right-dark' required bind:value={search.season} disabled={search.disableSearch}>
<option value selected disabled hidden>Any</option>
<option value='WINTER'>Winter</option>
<option value='SPRING'>Spring</option>
@ -132,7 +115,7 @@
<option>{year}</option>
{/each}
</datalist>
<input type='number' placeholder='Any' min='1940' max='2100' list='search-year' class='bg-dark-light form-control' bind:value={search.year} />
<input type='number' placeholder='Any' min='1940' max='2100' list='search-year' class='bg-dark-light form-control' disabled={search.disableSearch} bind:value={search.year} />
</div>
</div>
<div class='col p-10 d-flex flex-column justify-content-end'>
@ -141,7 +124,7 @@
Format
</div>
<div class='input-group'>
<select class='form-control bg-dark-light' required bind:value={search.format}>
<select class='form-control bg-dark-light' required bind:value={search.format} disabled={search.disableSearch}>
<option value selected disabled hidden>Any</option>
<option value='TV'>TV Show</option>
<option value='MOVIE'>Movie</option>
@ -157,7 +140,7 @@
Status
</div>
<div class='input-group'>
<select class='form-control bg-dark-light' required bind:value={search.status}>
<select class='form-control bg-dark-light' required bind:value={search.status} disabled={search.disableSearch}>
<option value selected disabled hidden>Any</option>
<option value='RELEASING'>Airing</option>
<option value='FINISHED'>Finished</option>
@ -172,7 +155,7 @@
Sort
</div>
<div class='input-group'>
<select class='form-control bg-dark-light' required bind:value={search.sort}>
<select class='form-control bg-dark-light' required bind:value={search.sort} disabled={search.disableSearch}>
<option value selected disabled hidden>Name</option>
<option value='START_DATE_DESC'>Release Date</option>
<option value='SCORE_DESC'>Score</option>
@ -194,7 +177,7 @@
</div>
<div class='col-auto p-10 d-flex'>
<div class='align-self-end'>
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' on:click={searchClear} class:text-primary={!!current}>
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' on:click={searchClear} class:text-primary={!!sanitisedSearch?.length}>
delete
</button>
</div>
@ -204,7 +187,7 @@
{#if sanitisedSearch?.length}
<span class='material-symbols-outlined font-size-24 mr-20 filled text-dark-light'>sell</span>
{#each sanitisedSearch as badge}
<span class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white'>{('' + badge).replace(/_/g, ' ').toLowerCase()}</span>
<span class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white text-nowrap'>{('' + badge).replace(/_/g, ' ').toLowerCase()}</span>
{/each}
{/if}
<span class='material-symbols-outlined font-size-24 mr-10 filled ml-auto text-dark-light'>grid_on</span>

View file

@ -6,7 +6,6 @@
import { addToast } from './Toasts.svelte'
import { wrapEnter } from '@/modules/util.js'
const view = getContext('view')
const gallery = getContext('gallery')
export let page
const links = [
{
@ -34,15 +33,20 @@
{
click: () => {
page = 'home'
$gallery = null
},
icon: 'home',
text: 'Home'
},
{
click: () => {
page = 'search'
},
icon: 'search',
text: 'Search'
},
{
click: () => {
page = 'home'
$gallery = 'schedule'
},
icon: 'schedule',
text: 'Schedule'

View file

@ -1,40 +1,70 @@
import { mapBestRelease } from '../anime.js'
import { fastPrettyBytes } from '../util.js'
import { exclusions } from '../rss.js'
import { set } from '@/views/Settings.svelte'
const toshoURL = decodeURIComponent(atob('aHR0cHM6Ly9mZWVkLmFuaW1ldG9zaG8ub3JnL2pzb24/'))
// TODO query resolution
export default async function ({ media, episode }) {
const mappings = await fetch('https://api.ani.zip/mappings?anilist_id=' + media.id + '&qx=' + exclusions.map(e => '!' + e).join(' '))
const mappings = await fetch('https://api.ani.zip/mappings?anilist_id=' + media.id)
const { episodes, mappings: map } = await mappings.json()
const entries = []
let promises = []
if (episodes[Number(episode)]) {
const { anidbEid } = episodes[Number(episode)]
const torrents = await fetch(toshoURL + 'eid=' + anidbEid)
entries.push(...await torrents.json())
promises.push(fetchToshoEntries({ media, id: anidbEid, quality: set.rssQuality }))
} else {
// TODO: look for episodes via.... title?
}
// look for batches
if (map.anidb_id && (media.status === 'FINISHED' || media.episodes === 1)) {
const torrents = await fetch(toshoURL + 'aid=' + map.anidb_id + '&order=size-d' + '&qx=' + exclusions.map(e => '!' + e).join(' '))
const batches = (await torrents.json()).filter(entry => {
return entry.num_files >= media.episodes
})
entries.push(...batches)
promises.push(fetchToshoEntries({ media, id: map.anidb_id, quality: set.rssQuality, batch: true }))
}
let entries = (await Promise.all(promises)).flat()
if (!entries.length) {
promises = []
if (episodes[Number(episode)]) {
const { anidbEid } = episodes[Number(episode)]
promises.push(fetchToshoEntries({ media, id: anidbEid }))
} else {
// TODO: look for episodes via.... title?
}
// look for batches
if (map.anidb_id && (media.status === 'FINISHED' || media.episodes === 1)) {
promises.push(fetchToshoEntries({ media, id: map.anidb_id, batch: true }))
}
}
entries = (await Promise.all(promises)).flat()
const mapped = mapTosho2dDeDupedEntry(entries)
return mapBestRelease(mapped)
}
async function fetchToshoEntries ({ media, id, batch, quality }) {
const exclusionsString = `!("${exclusions.join('"|"')}")`
const qualityString = quality ? ' "' + quality + '"' : ''
const queryString = '&qx=1&q=' + exclusionsString + qualityString
if (batch) {
const torrents = await fetch(toshoURL + 'aid=' + id + '&order=size-d' + queryString)
return (await torrents.json()).filter(entry => {
return entry.num_files >= media.episodes
})
} else {
const torrents = await fetch(toshoURL + 'eid=' + id + queryString)
return torrents.json()
}
}
function mapTosho2dDeDupedEntry (entries) {
const deduped = {}
for (const entry of entries) {
@ -42,7 +72,7 @@ function mapTosho2dDeDupedEntry (entries) {
const dupe = deduped[entry.info_hash]
dupe.title ??= entry.torrent_name || entry.title
dupe.id ||= entry.nyaa_id
dupe.seeders ||= entry.seeders ?? 0
dupe.seeders ||= entry.seeders >= 100000 ? entry.leechers * 3 : entry.seeders
dupe.leechers ||= entry.leechers ?? 0
dupe.size ||= entry.total_size && fastPrettyBytes(entry.total_size)
dupe.date ||= entry.timestamp && new Date(entry.timestamp * 1000)
@ -51,7 +81,7 @@ function mapTosho2dDeDupedEntry (entries) {
title: entry.torrent_name || entry.title,
link: entry.magnet_uri,
id: entry.nyaa_id,
seeders: entry.seeders,
seeders: entry.seeders >= 100000 ? entry.leechers * 3 : entry.seeders, // this is a REALLY bad assumption to make, but its a decent guess
leechers: entry.leechers,
size: entry.total_size && fastPrettyBytes(entry.total_size),
date: entry.timestamp && new Date(entry.timestamp * 1000)

View file

@ -3,37 +3,36 @@ import { alRequest } from '@/modules/anilist.js'
export default class Sections {
constructor (data = []) {
this.sections = []
this.search = {}
this.add(data)
}
add (data) {
for (const { title, variables = {}, type, load = this.createFallbackLoad(variables, type), preview } of data) {
this.sections.push({ load, title, preview })
for (const { title, variables = {}, type, load = Sections.createFallbackLoad(variables, type), preview } of data) {
this.sections.push({ load, title, preview, variables })
}
}
createFallbackLoad (variables, type) {
return (page = 1, perPage = 50) => {
const options = { method: 'Search', page, perPage, ...variables, ...this.sanitiseObject(this.search) }
static createFallbackLoad (variables, type) {
return (page = 1, perPage = 50, search = variables) => {
const options = { method: 'Search', page, perPage, ...Sections.sanitiseObject(search) }
const res = alRequest(options)
return this.wrapResponse(res, perPage, type)
return Sections.wrapResponse(res, perPage, type)
}
}
wrapResponse (res, length, type) {
static wrapResponse (res, length, type) {
res.then(res => {
this.hasNext = res?.data?.Page.pageInfo.hasNextPage
})
return Array.from({ length }, (_, i) => ({ type, data: this.fromPending(res, i) }))
return Array.from({ length }, (_, i) => ({ type, data: Sections.fromPending(res, i) }))
}
async fromPending (arr, i) {
static async fromPending (arr, i) {
const { data } = await arr
return data.Page.media[i]
}
sanitiseObject (object) {
static sanitiseObject (object = {}) {
const safe = {}
for (const [key, value] of Object.entries(object)) {
if (value) safe[key] = value

View file

@ -100,7 +100,6 @@
import Section from './Section.svelte'
import Banner from '@/components/banner/Banner.svelte'
import smoothScroll from '@/modules/scroll.js'
export let current
</script>
<div class='h-full w-full overflow-y-scroll root overflow-x-hidden' use:smoothScroll>

View file

@ -4,6 +4,8 @@
<script>
import Card from '@/components/cards/Card.svelte'
import { search } from '../Search.svelte'
import { page } from '@/App.svelte'
import { wrapEnter } from '@/modules/util.js'
export let opts
@ -14,13 +16,21 @@
if (!opts.preview) opts.preview = opts.load(1, 10)
observer.unobserve(element)
}
}, { hreshold: 0 })
}, { threshold: 0 })
observer.observe(element)
}
function click () {
$search = {
...opts.variables,
load: opts.load
}
$page = 'search'
}
</script>
<span class='d-flex px-20 align-items-end pointer text-decoration-none text-muted'
on:click={opts.onclick} on:keydown={wrapEnter(opts.onclick)}
on:click={click} on:keydown={wrapEnter(click)}
tabindex='0'
use:deferredLoad
role='button'>

View file

@ -124,6 +124,12 @@
}
}
let loadInterval
function clearLoadInterval () {
clearInterval(loadInterval)
}
async function handleCurrent (file) {
if (file) {
if (thumbnailData.video?.src) URL.revokeObjectURL(video?.src)
@ -143,8 +149,9 @@
src = file.url
client.send('current', file)
subs = new Subtitles(video, files, current, handleHeaders)
await tick()
video?.play()
loadInterval = setInterval(() => {
if (!video.readyState) video.load()
}, 200)
}
}
@ -916,6 +923,7 @@
on:loadedmetadata={findChapters}
on:loadedmetadata={autoPlay}
on:loadedmetadata={checkAudio}
on:loadedmetadata={clearLoadInterval}
on:leavepictureinpicture={() => (pip = false)} />
{#if stats}
<div class='position-absolute top-0 bg-tp p-10 m-15 text-monospace rounded z-50'>

View file

@ -1,13 +1,27 @@
<script context='module'>
import { writable } from 'simple-store-svelte'
import Sections from '@/modules/sections.js'
export const search = writable(null)
export const search = writable({})
let items = []
</script>
<script>
import Search from '../components/Search.svelte'
import Search, { searchCleanup } from '../components/Search.svelte'
import Card from '../components/cards/Card.svelte'
import smoothScroll from '@/modules/scroll.js'
const fallbackLoad = Sections.createFallbackLoad()
const load = $search.load || fallbackLoad
items = load(1, 50, searchCleanup($search))
</script>
{#if $search}
<Search />
{/if}
<Search bind:search={$search} />
<div class='h-full w-full overflow-y-scroll d-flex flex-wrap flex-row root overflow-x-hidden px-20 justify-content-center' use:smoothScroll>
{#each items as card}
<Card {card} />
{/each}
</div>