feat: improved batch support and handling

This commit is contained in:
ThaUnknown 2022-08-01 12:47:58 +02:00
parent 000f39ced0
commit 057385aed6
12 changed files with 219 additions and 151 deletions

View file

@ -1,6 +1,6 @@
{
"name": "Miru",
"version": "2.10.5",
"version": "2.11.0",
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
"main": "src/index.js",
"homepage": "https://github.com/ThaUnknown/miru#readme",

View file

@ -31,7 +31,7 @@ export let length = 5
<div class='row h-full'>
<div class='col-4 skeloader' />
<div class='col-8 bg-very-dark px-15 py-10'>
<h5 class='m-0 text-capitalize font-weight-bold pb-10'>{[card.parseObject.anime_title, card.parseObject.episode_number].filter(s => s).join(' - ')}</h5>
<h5 class='m-0 text-capitalize font-weight-bold pb-10'>{[card.parseObject.anime_title, card.episode].filter(s => s).join(' - ')}</h5>
<p class='skeloader w-150 h-10 rounded bg-dark' />
<p class='skeloader w-150 h-10 rounded bg-dark' />
</div>
@ -49,7 +49,7 @@ export let length = 5
{#if card.failed}
<span class='badge badge-secondary'>Uncertain</span>
{/if}
{[card.media.title.userPreferred, card.episodeNumber].filter(s => s).join(' - ')}
{[card.media.title.userPreferred, card.episode].filter(s => s).join(' - ')}
</h5>
{#if card.schedule && card.media.nextAiringEpisode}
<span class='text-muted font-weight-bold'>
@ -76,7 +76,7 @@ export let length = 5
</p>
</div>
<div class='overflow-y-auto px-15 pb-5 bg-very-dark card-desc pre-wrap'>
{card.media.description.replace(/<[^>]*>/g, '')}
{card.media.description?.replace(/<[^>]*>/g, '') || ''}
</div>
<div class='px-15 pb-10 pt-5 genres'>
{#each card.media.genres as genre}

View file

@ -115,7 +115,7 @@ async function releasesCards (page, limit, force, val) {
const index = (page - 1) * limit
const items = [...doc.querySelectorAll('item')].slice(index, index + limit)
hasNext = items.length === limit
const media = await resolveFileMedia({ fileName: items.map(item => item.querySelector('title').textContent), isRelease: true })
const media = await resolveFileMedia(items.map(item => item.querySelector('title').textContent))
media.forEach((mediaInformation, index) => {
mediaInformation.onclick = () => {
add(items[index].querySelector('link').textContent)

View file

@ -0,0 +1,168 @@
<script context='module'>
import { writable, get } from 'svelte/store'
import { resolveFileMedia } from '@/modules/anime.js'
import { videoRx } from '@/modules/util.js'
import { title } from '../Menubar.svelte'
const episodeRx = /Episode (\d+) - (.*)/
export const media = writable(null)
const nowPlaying = writable({})
export const files = writable([])
const processed = writable([])
const noop = () => {}
let playFile = noop
media.subscribe((media) => {
handleMedia(media || {})
return noop
})
function handleCurrent ({ detail }) {
media.set(detail.media)
}
export function findInCurrent (obj) {
const oldNowPlaying = get(nowPlaying)
const fileList = get(files)
const targetFile = fileList.find(file => file.media.media.id === obj.media.id && file.media.episode === obj.episode)
if (!targetFile) return false
if (oldNowPlaying.media.id !== obj.media.id) {
// mediachange, filelist change
media.set({ media: obj.media, episode: obj.episode })
handleFiles(fileList)
} else {
playFile(targetFile)
}
return true
}
function handleMedia ({ media, episode, parseObject }) {
if (media) {
const ep = Number(episode || parseObject.episode_number) || null
const streamingEpisode = media?.streamingEpisodes.find(episode => {
const match = episodeRx.exec(episode.title)
return match && Number(match[1]) === ep
})
const np = {
media,
title: media?.title.userPreferred || parseObject.anime_title,
episode: ep,
episodeTitle: streamingEpisode && episodeRx.exec(streamingEpisode.title)[2],
thumbnail: streamingEpisode?.thumbnail || media?.coverImage.extraLarge
}
setDiscordRPC(np)
setMediaSession(np)
nowPlaying.set(np)
}
}
async function handleFiles (files) {
let videoFiles = []
const otherFiles = []
for (const file of files) {
if (videoRx.test(file.name)) {
videoFiles.push(file)
} else {
otherFiles.push(file)
}
}
const resolved = await resolveFileMedia(videoFiles.map(file => file.name))
videoFiles.map(file => {
file.media = resolved.find(({ parseObject }) => file.name.includes(parseObject.file_name))
return file
})
const nowPlaying = get(media)
if (nowPlaying?.media) videoFiles = videoFiles.filter(file => file.media.media.id === nowPlaying.media.id)
videoFiles.sort((a, b) => a.media.episode - b.media.episode)
if (!videoFiles.length) {
processed.set(files)
} else {
processed.set([...videoFiles, ...otherFiles])
if (nowPlaying?.episode && nowPlaying.episode !== 1) {
const file = videoFiles.find(({ media }) => media.episode === nowPlaying.episode)
playFile(file || 0)
}
}
}
files.subscribe((files = []) => {
handleFiles(files)
return noop
})
function setMediaSession (nowPlaying) {
const name = [nowPlaying.title, nowPlaying.episode, nowPlaying.episodeTitle, 'Miru'].filter(i => i).join(' - ')
title.set(name)
const metadata =
nowPlaying.thumbnail
? new MediaMetadata({
title: name,
artwork: [
{
src: nowPlaying.thumbnail,
sizes: '256x256',
type: 'image/jpg'
}
]
})
: new MediaMetadata({ title: name })
navigator.mediaSession.metadata = metadata
}
function setDiscordRPC (nowPlaying) {
window.IPC.emit('discord', {
activity: {
details: [nowPlaying.title, nowPlaying.episodeTitle].filter(i => i).join(' - '),
state: 'Watching Episode ' + ((!nowPlaying.media?.episodes && nowPlaying.episode) || ''),
timestamps: {
start: Date.now()
},
party: {
size: (nowPlaying.episode && nowPlaying.media?.episodes && [nowPlaying.episode, nowPlaying.media.episodes]) || undefined
},
assets: {
large_text: nowPlaying.title,
large_image: nowPlaying.thumbnail,
small_image: 'logo',
small_text: 'https://github.com/ThaUnknown/miru'
},
instance: true,
type: 3,
buttons: [
{
label: 'Download app',
url: 'https://github.com/ThaUnknown/miru/releases/latest'
},
{
label: 'Watch on Miru',
url: `miru://anime/${nowPlaying.media?.id}`
}
]
}
})
}
</script>
<script>
import Player from './Player.svelte'
export let miniplayer = false
export let page = 'home'
</script>
<Player files={$processed} {miniplayer} media={$nowPlaying} bind:playFile bind:page on:current={handleCurrent}/>

View file

@ -1,77 +1,9 @@
<script context='module'>
<script>
import { set } from '../Settings.svelte'
import { playAnime } from '../RSSView.svelte'
import { title } from '../Menubar.svelte'
import { onMount } from 'svelte'
import { client } from '@/modules/torrent.js'
export let media = null
let fileMedia = null
let hadImage = false
export function updateMedia (fileMed) {
if (!fileMedia) {
setDiscordRPC(fileMed)
}
fileMedia = fileMed
media = fileMedia.media
const name = [fileMedia.mediaTitle, fileMedia.episodeNumber, fileMedia.episodeTitle, 'Miru'].filter(i => i).join(' - ')
title.set(name)
fileMedia.episodeThumbnail = !!fileMedia.episodeThumbnail
const metadata =
fileMedia.episodeThumbnail || fileMedia.mediaCover
? new MediaMetadata({
title: name,
artwork: [
{
src: fileMedia.episodeThumbnail || fileMedia.mediaCover,
sizes: '256x256',
type: 'image/jpg'
}
]
})
: new MediaMetadata({ title: name })
if (fileMedia.parseObject?.release_group) metadata.artist = fileMedia.parseObject.release_group
navigator.mediaSession.metadata = metadata
}
function setDiscordRPC (fileMedia) {
if (fileMedia) {
window.IPC.emit('discord', {
activity: {
details: fileMedia.media?.title.userPreferred || fileMedia.parseObject.anime_title,
state: 'Watching Episode ' + ((!fileMedia.media?.episodes && fileMedia.episodeNumber) || ''),
timestamps: {
start: Date.now()
},
party: {
size: (fileMedia.episodeNumber && fileMedia.media?.episodes && [fileMedia.episodeNumber, fileMedia.media.episodes]) || undefined
},
assets: {
large_text: fileMedia.media?.title.userPreferred,
large_image: fileMedia.media?.coverImage.extraLarge,
small_image: 'logo',
small_text: 'https://github.com/ThaUnknown/miru'
},
instance: true,
type: 3,
buttons: [
{
label: 'Download app',
url: 'https://github.com/ThaUnknown/miru/releases/latest'
},
{
label: 'Watch on Miru',
url: `miru://anime/${fileMedia.media?.id}`
}
]
}
})
}
}
</script>
<script>
import { onMount, createEventDispatcher } from 'svelte'
import { alEntry } from '@/modules/anilist.js'
import { resolveFileMedia } from '@/modules/anime.js'
// import Peer from '@/modules/Peer.js'
import Subtitles from '@/modules/subtitles.js'
import { toTS, videoRx, fastPrettyBytes } from '@/modules/util.js'
@ -80,31 +12,29 @@ import { addToast } from '../Toasts.svelte'
import { w2gEmitter } from '../WatchTogether/WatchTogether.svelte'
import Keybinds, { loadWithDefaults, condition } from 'svelte-keybinds'
const emit = createEventDispatcher()
w2gEmitter.on('playerupdate', ({ detail }) => {
currentTime = detail.time
paused = detail.paused
})
w2gEmitter.on('setindex', ({ detail }) => {
handleCurrent(videos?.[detail])
playFile(detail)
})
export function playFile (file) {
if (typeof value === 'number') {
handleCurrent(videos?.[file])
} else if (videos.includes(file)) {
handleCurrent(file)
}
}
function updatew2g () {
w2gEmitter.emit('player', { time: Math.floor(currentTime), paused })
}
async function mediaChange (current, image) {
if (current && 'mediaSession' in navigator) {
if (!media || (!hadImage && image)) {
// filename is already mapped so this *should* be fine
const data = await resolveFileMedia({ fileName: current.name })
if (image) data.episodeThumbnail = image
updateMedia(data)
checkAvail(current)
}
}
}
export let miniplayer = false
// eslint-disable-next-line prefer-const
$condition = () => !miniplayer
@ -137,6 +67,8 @@ let volume = localStorage.getItem('volume') || 1
let playbackRate = 1
$: localStorage.setItem('volume', volume || 0)
export let media
function checkAudio () {
if ('audioTracks' in HTMLVideoElement.prototype && !video.audioTracks.length) {
addToast({
@ -230,9 +162,6 @@ function updateFiles (files) {
}
}
} else {
media = null
fileMedia = null
hadImage = false
src = ''
video?.load()
currentTime = 0
@ -250,11 +179,9 @@ async function handleCurrent (file) {
})
currentTime = 0
targetTime = 0
media = null
fileMedia = null
hadImage = false
completed = false
current = file
emit('current', current)
initSubs()
src = file.url
client.send('current', file)
@ -267,14 +194,14 @@ async function handleCurrent (file) {
let hasNext = false
let hasLast = false
function checkAvail (current) {
if ((media?.nextAiringEpisode?.episode - 1 || media?.episodes) > fileMedia?.episodeNumber) {
if ((media?.media?.nextAiringEpisode?.episode - 1 || media?.media?.episodes) > media?.episode) {
hasNext = true
} else if (videos.indexOf(current) !== videos.length - 1) {
hasNext = true
} else {
hasNext = false
}
if (media && fileMedia?.episodeNumber > 1) {
if (media?.episode > 1) {
hasLast = true
} else if (videos.indexOf(current) > 0) {
hasLast = true
@ -347,8 +274,8 @@ function playNext () {
const target = (index + 1) % videos.length
handleCurrent(videos[target])
w2gEmitter.emit('index', { index: target })
} else if (media?.nextAiringEpisode?.episode - 1 || media?.episodes > fileMedia?.episodeNumber) {
playAnime(media, fileMedia?.episodeNumber + 1)
} else if (media?.media?.nextAiringEpisode?.episode - 1 || media?.media?.episodes > media?.episode) {
playAnime(media.media, media.episode + 1)
}
}
}
@ -358,8 +285,8 @@ function playLast () {
if (index > 1) {
handleCurrent(videos[index - 1])
w2gEmitter.emit('index', { index: index - 1 })
} else if (media && fileMedia?.episodeNumber > 1) {
playAnime(media, fileMedia?.episodeNumber - 1)
} else if (media?.episode > 1) {
playAnime(media.media, media.episode - 1)
}
}
}
@ -692,7 +619,6 @@ $: navigator.mediaSession?.setPositionState({
playbackRate: 1,
position: Math.max(0, Math.min(duration || 0, currentTime || 0))
})
$: mediaChange(current)
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', playPause)
@ -768,7 +694,6 @@ function createThumbnail (vid = video) {
thumbnailData.context.fillRect(0, 0, 200, thumbnailData.canvas.height)
thumbnailData.context.drawImage(vid, 0, 0, 200, thumbnailData.canvas.height)
thumbnailData.thumbnails[index] = thumbnailData.canvas.toDataURL('image/jpeg')
if (index === 5) mediaChange(current, thumbnailData.thumbnails[index])
}
}
}
@ -846,10 +771,10 @@ function toggleDropdown ({ target }) {
let completed = false
function checkCompletion () {
if (!completed && duration && video?.readyState && duration - 180 < currentTime) {
if (fileMedia?.media?.episodes || fileMedia?.media?.nextAiringEpisode?.episode) {
if (fileMedia.media.episodes || fileMedia.media.nextAiringEpisode?.episode > fileMedia.episodeNumber) {
if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) {
if (media.media.episodes || media.media.nextAiringEpisode?.episode > media.episode) {
completed = true
alEntry(fileMedia)
alEntry(media)
}
}
}

View file

@ -1,10 +1,10 @@
<script context='module'>
import { DOMPARSER } from '@/modules/util.js'
import { updateMedia } from './Player/Player.svelte'
import { set } from './Settings.svelte'
import { addToast } from './Toasts.svelte'
import { alRequest } from '@/modules/anilist.js'
import { episodeRx, findEdge, resolveSeason, getMediaMaxEp } from '@/modules/anime.js'
import { findEdge, resolveSeason, getMediaMaxEp } from '@/modules/anime.js'
import { findInCurrent } from './Player/MediaHandler.svelte'
import { writable } from 'svelte/store'
@ -26,6 +26,7 @@ video.remove()
export function playAnime (media, episode = 1) {
episode = isNaN(episode) ? 1 : episode
if (findInCurrent({ media, episode })) return
rss.set({ media, episode })
}
@ -216,13 +217,12 @@ function createTitle (media) {
<script>
import { add } from '@/modules/torrent.js'
import { media } from './Player/MediaHandler.svelte'
$: parseRss($rss)
let table = null
let fileMedia = null
export async function parseRss ({ media, episode }) {
if (!media) return
const entries = await getRSSEntries({ media, episode })
@ -235,16 +235,6 @@ export async function parseRss ({ media, episode }) {
return
}
entries.sort((a, b) => b.seeders - a.seeders)
const streamingEpisode = media?.streamingEpisodes.filter(episode => episodeRx.exec(episode.title) && Number(episodeRx.exec(episode.title)[1]) === Number(episode))[0]
fileMedia = {
mediaTitle: media?.title.userPreferred,
episodeNumber: Number(episode),
episodeTitle: streamingEpisode ? episodeRx.exec(streamingEpisode.title)[2] : undefined,
episodeThumbnail: streamingEpisode?.thumbnail,
mediaCover: media?.coverImage.medium,
name: 'Miru',
media
}
if (settings.rssAutoplay) {
play(entries[0])
} else {
@ -258,7 +248,7 @@ function checkClose ({ keyCode }) {
if (keyCode === 27) close()
}
function play (entry) {
updateMedia(fileMedia)
$media = $rss
if (entry.seeders !== '?' && entry.seeders <= 15) {
addToast({
text: 'This release is poorly seeded and likely will have playback issues such as buffering!',

View file

@ -1,13 +1,7 @@
<script context='module'>
import { writable } from 'svelte/store'
export const files = writable([])
</script>
<script>
import { getContext } from 'svelte'
import Home from './Home/Home.svelte'
import Player from './Player/Player.svelte'
import MediaHandler from './Player/MediaHandler.svelte'
import Settings from './Settings.svelte'
import WatchTogether from './WatchTogether/WatchTogether.svelte'
import Miniplayer from 'svelte-miniplayer'
@ -17,7 +11,7 @@ const current = getContext('gallery')
<div class='overflow-hidden content-wrapper'>
<Miniplayer active={page !== 'player'} class='bg-dark z-10 {page === 'player' ? 'h-full' : ''}' minwidth='35rem' maxwidth='45rem' width='300px' padding='2rem'>
<Player files={$files} miniplayer={page !== 'player'} bind:page />
<MediaHandler miniplayer={page !== 'player'} bind:page />
</Miniplayer>
{#if page === 'settings'}
<Settings />

View file

@ -1,7 +1,7 @@
<script>
import { getContext } from 'svelte'
import { alID } from '@/modules/anilist.js'
import { media } from './Player/Player.svelte'
import { media } from './Player/MediaHandler.svelte'
import { platformMap } from './Settings.svelte'
import { addToast } from './Toasts.svelte'
const sidebar = getContext('sidebar')
@ -35,7 +35,7 @@ const links = [
},
{
click: () => {
if (media) $view = media
if ($media) $view = $media.media
},
icon: 'queue_music',
text: 'Now Playing'

View file

@ -84,7 +84,7 @@ function checkClose ({ keyCode }) {
<div class='col-md-9 px-20'>
<h1 class='title font-weight-bold text-white'>Synopsis</h1>
<div class='font-size-16 pr-15 pre-wrap'>
{media.description.replace(/<[^>]*>/g, '')}
{media.description?.replace(/<[^>]*>/g, '') || ''}
</div>
<ToggleList list={media.relations?.edges?.filter(({ node }) => node.type === 'ANIME')} let:item>
<div class='w-150 mx-15 mb-10 rel pointer' on:click={async () => { $view = null; $view = (await alRequest({ method: 'SearchIDSingle', id: item.node.id })).data.Media }}>

View file

@ -104,7 +104,7 @@ export function alEntry (filemedia) {
if (media.status === 'FINISHED' || media.status === 'RELEASING') {
// some anime/OVA's can have a single episode, or some movies can have multiple episodes
const singleEpisode = (!media.episodes || (media.format === 'MOVIE' && media.episodes === 1)) && 1
const videoEpisode = Number(filemedia.episodeNumber) || singleEpisode
const videoEpisode = Number(filemedia.episode) || singleEpisode
const mediaEpisode = media.nextAiringEpisode?.episode || media.episodes || singleEpisode
// check episode range
if (videoEpisode && mediaEpisode && mediaEpisode >= videoEpisode) {
@ -113,7 +113,7 @@ export function alEntry (filemedia) {
if (!media.mediaListEntry || media.mediaListEntry?.progress <= videoEpisode || singleEpisode) {
const variables = {
method: 'Entry',
repeat: 0,
repeat: media.mediaListEntry?.repeat || 0,
id: media.id,
status: 'CURRENT',
episode: videoEpisode,
@ -121,7 +121,7 @@ export function alEntry (filemedia) {
}
if (videoEpisode === mediaEpisode) {
variables.status = 'COMPLETED'
if (media.mediaListEntry?.status === 'COMPLETED' || media.mediaListEntry.status === 'REPEATING') variables.repeat = media.mediaListEntry.repeat + 1
if (media.mediaListEntry.status === 'REPEATING') variables.repeat = media.mediaListEntry.repeat + 1
}
if (!lists.includes('Watched using Miru')) {
variables.lists.push('Watched using Miru')

View file

@ -83,8 +83,6 @@ export function getMediaMaxEp (media, playable) {
}
}
export const episodeRx = /Episode (\d+) - (.*)/
// resolve anime name based on file name and store it
const postfix = {
1: 'st',
@ -160,12 +158,10 @@ function getParseObjTitle (obj) {
return title
}
export async function resolveFileMedia (opts) {
// opts.fileName
const parsePromises = opts.fileName.constructor === Array
? opts.fileName.map(name => anitomyscript(name))
: [anitomyscript(opts.fileName)]
const parseObjs = await Promise.all(parsePromises)
export async function resolveFileMedia (fileName) {
let parseObjs = await anitomyscript(fileName)
if (parseObjs.constructor !== Array) parseObjs = [parseObjs]
// batches promises in 10 at a time, because of CF burst protection, which still sometimes gets triggered :/
await PromiseBatch(resolveTitle, [...new Set(parseObjs.map(obj => getParseObjTitle(obj)))].filter(title => !(title in relations)), 10)
const fileMedias = []
@ -218,20 +214,14 @@ export async function resolveFileMedia (opts) {
}
}
}
const streamingEpisode = media?.streamingEpisodes.filter(episode => episodeRx.exec(episode.title) && Number(episodeRx.exec(episode.title)[1]) === Number(parseObj.episode_number))[0]
fileMedias.push({
mediaTitle: media?.title.userPreferred || parseObj.anime_title,
episodeNumber: episode || parseObj.episode_number,
episodeTitle: streamingEpisode && episodeRx.exec(streamingEpisode.title)[2],
episodeThumbnail: streamingEpisode?.thumbnail,
mediaCover: media?.coverImage.medium,
name: 'Miru',
episode: episode || parseObj.episode_number,
parseObject: parseObj,
media,
failed
})
}
return fileMedias.length === 1 ? fileMedias[0] : fileMedias
return fileMedias
}
export function findEdge (media, type, formats = ['TV', 'TV_SHORT'], skip) {

View file

@ -1,7 +1,8 @@
// import WebTorrent from 'webtorrent'
import { set } from '@/lib/Settings.svelte'
import { files } from '@/lib/Router.svelte'
import { files } from '@/lib/Player/MediaHandler.svelte'
import { page } from '@/App.svelte'
import 'browser-event-target-emitter'
class TorrentWorker extends Worker {
constructor (opts) {