mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 09:42:05 +00:00
feat: tosho quality search
fix: player load stuck wip: search
This commit is contained in:
parent
98beb4e8e4
commit
851281b2f3
11 changed files with 130 additions and 86 deletions
|
|
@ -25,8 +25,6 @@
|
|||
|
||||
setContext('view', view)
|
||||
|
||||
setContext('gallery', writable(null))
|
||||
|
||||
setContext('trailer', writable(null))
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue