mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-18 14:12:04 +00:00
fix: discord RPC, file episode finder/picker
feat: title on releases cards, NC-Raws
This commit is contained in:
parent
9e5b239ea3
commit
5f4e711254
5 changed files with 183 additions and 175 deletions
|
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class='card m-0 p-0' on:click={card.onclick || (() => viewMedia(card.media))} style:--color={card.media.coverImage.color || '#1890ff'}>
|
||||
<div class='card m-0 p-0' on:click={card.onclick || (() => viewMedia(card.media))} style:--color={card.media.coverImage.color || '#1890ff'} title={card.parseObject?.file_name}>
|
||||
<div class='row h-full'>
|
||||
<div class='col-4'>
|
||||
<img loading='lazy' src={card.media.coverImage.extraLarge || ''} alt='cover' class='cover-img w-full h-full' />
|
||||
|
|
|
|||
|
|
@ -1,198 +1,206 @@
|
|||
<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'
|
||||
import { tick } from 'svelte'
|
||||
import { state } from '../WatchTogether/WatchTogether.svelte'
|
||||
import { writable, get } from 'svelte/store'
|
||||
import { resolveFileMedia } from '@/modules/anime.js'
|
||||
import { videoRx } from '@/modules/util.js'
|
||||
import { title } from '../Menubar.svelte'
|
||||
import { tick } from 'svelte'
|
||||
import { state } from '../WatchTogether/WatchTogether.svelte'
|
||||
|
||||
const episodeRx = /Episode (\d+) - (.*)/
|
||||
const episodeRx = /Episode (\d+) - (.*)/
|
||||
|
||||
export const media = writable(null)
|
||||
export const media = writable(null)
|
||||
|
||||
const nowPlaying = writable({})
|
||||
const nowPlaying = writable({})
|
||||
|
||||
export const files = writable([])
|
||||
export const files = writable([])
|
||||
|
||||
const processed = writable([])
|
||||
const processed = writable([])
|
||||
|
||||
const noop = () => {}
|
||||
const noop = () => {}
|
||||
|
||||
let playFile
|
||||
let playFile
|
||||
|
||||
media.subscribe((media) => {
|
||||
handleMedia(media || {})
|
||||
return noop
|
||||
})
|
||||
|
||||
function handleCurrent ({ detail }) {
|
||||
media.set(detail.media)
|
||||
}
|
||||
|
||||
export function findInCurrent (obj) {
|
||||
const oldNowPlaying = get(nowPlaying)
|
||||
|
||||
if (oldNowPlaying.media?.id === obj.media.id && oldNowPlaying.episode === obj.episode) return false
|
||||
|
||||
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) {
|
||||
if (!files?.length) return processed.set(files)
|
||||
const 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
|
||||
media.subscribe((media) => {
|
||||
handleMedia(media || {})
|
||||
return noop
|
||||
})
|
||||
|
||||
let nowPlaying = get(media)
|
||||
|
||||
if (!nowPlaying) {
|
||||
const max = highest(videoFiles, (file) => file?.media?.media?.id).media
|
||||
if (max?.media) {
|
||||
nowPlaying = { media: max.media, episode: (max.media.mediaListEntry?.progress + 1 || 1) }
|
||||
media.set(nowPlaying)
|
||||
}
|
||||
function handleCurrent ({ detail }) {
|
||||
media.set(detail.media)
|
||||
}
|
||||
|
||||
const filtered = nowPlaying?.media && videoFiles.filter(file => file.media?.media?.id && file.media?.media?.id === nowPlaying.media.id)
|
||||
export function findInCurrent (obj) {
|
||||
const oldNowPlaying = get(nowPlaying)
|
||||
|
||||
const result = (filtered?.length && filtered) || videoFiles
|
||||
if (oldNowPlaying.media?.id === obj.media.id && oldNowPlaying.episode === obj.episode) return false
|
||||
|
||||
result.sort((a, b) => a.media.episode - b.media.episode)
|
||||
const fileList = get(files)
|
||||
|
||||
processed.set([...result, ...otherFiles])
|
||||
await tick()
|
||||
const file = nowPlaying?.episode && (result.find(({ media }) => media.episode === nowPlaying.episode) || result.find(({ media }) => media.episode === 1) || 0)
|
||||
playFile(file || 0)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
const highest = (arr = [], mapfn = a => a) => arr.reduce((acc, el) => {
|
||||
const mapped = mapfn(el)
|
||||
acc.sums[mapped] = (acc.sums[mapped] || 0) + 1
|
||||
acc.max = acc.sums[mapfn(acc.max)] > acc.sums[mapped] ? acc.max : el
|
||||
return acc
|
||||
}, { sums: {} }).max
|
||||
|
||||
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'
|
||||
}
|
||||
]
|
||||
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
|
||||
})
|
||||
: new MediaMetadata({ title: name })
|
||||
navigator.mediaSession.metadata = metadata
|
||||
}
|
||||
|
||||
function setDiscordRPC (np = get(nowPlaying)) {
|
||||
const w2g = get(state)
|
||||
const activity = {
|
||||
details: [np.title, np.episodeTitle].filter(i => i).join(' - '),
|
||||
state: 'Watching Episode ' + ((!np.media?.episodes && np.episode) || ''),
|
||||
timestamps: {
|
||||
start: Date.now()
|
||||
},
|
||||
party: {
|
||||
size: (np.episode && np.media?.episodes && [np.episode, np.media.episodes]) || undefined
|
||||
},
|
||||
assets: {
|
||||
large_text: np.title,
|
||||
large_image: np.thumbnail,
|
||||
small_image: 'logo',
|
||||
small_text: 'https://github.com/ThaUnknown/miru'
|
||||
},
|
||||
instance: true,
|
||||
type: 3
|
||||
}
|
||||
// cannot have buttons and secrets at once
|
||||
if (w2g) {
|
||||
activity.secrets = {
|
||||
join: w2g,
|
||||
match: w2g + 'm'
|
||||
}
|
||||
activity.party.id = w2g + 'p'
|
||||
} else {
|
||||
activity.buttons = [
|
||||
{
|
||||
label: 'Download app',
|
||||
url: 'https://github.com/ThaUnknown/miru/releases/latest'
|
||||
},
|
||||
{
|
||||
label: 'Watch on Miru',
|
||||
url: `miru://anime/${np.media?.id}`
|
||||
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)
|
||||
}
|
||||
}
|
||||
window.IPC.emit('discord', { activity })
|
||||
}
|
||||
state.subscribe(() => {
|
||||
setDiscordRPC()
|
||||
return noop
|
||||
})
|
||||
|
||||
async function handleFiles (files) {
|
||||
if (!files?.length) return processed.set(files)
|
||||
const 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
|
||||
})
|
||||
|
||||
let nowPlaying = get(media)
|
||||
|
||||
if (!nowPlaying) {
|
||||
const max = highestOccurence(videoFiles, file => file.media.media?.id).media
|
||||
if (max?.media) {
|
||||
nowPlaying = { media: max.media, episode: (max.media.mediaListEntry?.progress + 1 || 1) }
|
||||
media.set(nowPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = nowPlaying?.media && videoFiles.filter(file => file.media?.media?.id && file.media?.media?.id === nowPlaying.media.id)
|
||||
|
||||
let result
|
||||
if (filtered?.length) {
|
||||
result = filtered
|
||||
} else {
|
||||
const max = highestOccurence(videoFiles, file => file.media.parseObject.anime_title).media.parseObject.anime_title
|
||||
result = videoFiles.filter(file => file.media.parseObject.anime_title === max)
|
||||
}
|
||||
|
||||
result.sort((a, b) => a.media.episode - b.media.episode)
|
||||
|
||||
processed.set([...result, ...otherFiles])
|
||||
await tick()
|
||||
const file = nowPlaying?.episode && (result.find(({ media }) => media.episode === nowPlaying.episode) || result.find(({ media }) => media.episode === 1) || 0)
|
||||
playFile(file || 0)
|
||||
}
|
||||
|
||||
// find element with most occurences in array according to map function
|
||||
const highestOccurence = (arr = [], mapfn = a => a) => arr.reduce((acc, el) => {
|
||||
const mapped = mapfn(el)
|
||||
acc.sums[mapped] = (acc.sums[mapped] || 0) + 1
|
||||
acc.max = (acc.max !== undefined ? acc.sums[mapfn(acc.max)] : -1) > acc.sums[mapped] ? acc.max : el
|
||||
return acc
|
||||
}, { sums: {} }).max
|
||||
|
||||
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 (np = get(nowPlaying)) {
|
||||
const w2g = get(state)
|
||||
const details = [np.title, np.episodeTitle].filter(i => i).join(' - ') || undefined
|
||||
const activity = {
|
||||
details,
|
||||
state: details && 'Watching Episode ' + ((!np.media?.episodes && np.episode) || ''),
|
||||
timestamps: {
|
||||
start: Date.now()
|
||||
},
|
||||
party: {
|
||||
size: (np.episode && np.media?.episodes && [np.episode, np.media.episodes]) || undefined
|
||||
},
|
||||
assets: {
|
||||
large_text: np.title,
|
||||
large_image: np.thumbnail,
|
||||
small_image: 'logo',
|
||||
small_text: 'https://github.com/ThaUnknown/miru'
|
||||
},
|
||||
instance: true,
|
||||
type: 3
|
||||
}
|
||||
// cannot have buttons and secrets at once
|
||||
if (w2g) {
|
||||
activity.secrets = {
|
||||
join: w2g,
|
||||
match: w2g + 'm'
|
||||
}
|
||||
activity.party.id = w2g + 'p'
|
||||
} else {
|
||||
activity.buttons = [
|
||||
{
|
||||
label: 'Download app',
|
||||
url: 'https://github.com/ThaUnknown/miru/releases/latest'
|
||||
},
|
||||
{
|
||||
label: 'Watch on Miru',
|
||||
url: `miru://anime/${np.media?.id}`
|
||||
}
|
||||
]
|
||||
}
|
||||
window.IPC.emit('discord', { activity })
|
||||
}
|
||||
state.subscribe(() => {
|
||||
setDiscordRPC()
|
||||
return noop
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Player from './Player.svelte'
|
||||
import Player from './Player.svelte'
|
||||
|
||||
export let miniplayer = false
|
||||
export let page = 'home'
|
||||
export let miniplayer = false
|
||||
export let page = 'home'
|
||||
</script>
|
||||
|
||||
<Player files={$processed} {miniplayer} media={$nowPlaying} bind:playFile bind:page on:current={handleCurrent}/>
|
||||
<Player files={$processed} {miniplayer} media={$nowPlaying} bind:playFile bind:page on:current={handleCurrent} />
|
||||
|
|
|
|||
|
|
@ -1086,7 +1086,7 @@
|
|||
}
|
||||
|
||||
datalist option {
|
||||
background: #ff3c00;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
top: 5px;
|
||||
min-height: unset;
|
||||
height: 6px;
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
const rssmap = {
|
||||
SubsPlease: 'https://nyaa.si/?page=rss&c=0_0&f=0&u=subsplease&q=',
|
||||
'Erai-raws [Multi-Sub]': 'https://nyaa.si/?page=rss&c=0_0&f=0&u=Erai-raws&q=',
|
||||
NanDesuKa: 'https://nyaa.si/?page=rss&c=0_0&f=0&u=NanDesuKa&q='
|
||||
'NC-Raws': 'https://nyaa.si/?page=rss&c=0_0&f=0&u=BraveSail&q='
|
||||
}
|
||||
const epstring = ep => `"E${zeropad(ep)}+"|"E${zeropad(ep)}v"|"+${zeropad(ep)}+"|"+${zeropad(ep)}v"`
|
||||
export function getReleasesRSSurl (val) {
|
||||
|
|
|
|||
|
|
@ -202,8 +202,8 @@
|
|||
<input id='rss-feed-{i}' type='text' list='rss-feed-list-{i}' class='w-400 form-control form-control-lg' placeholder='https://nyaa.si/?page=rss&c=0_0&f=0&q=' autocomplete='off' bind:value={settings.rssFeeds[i][1]} />
|
||||
<datalist id='rss-feed-list-{i}'>
|
||||
<option value='SubsPlease'>https://nyaa.si/?page=rss&c=0_0&f=0&u=subsplease&q=</option>
|
||||
<option value='NC-Raws'>https://nyaa.si/?page=rss&c=0_0&f=0&u=BraveSail&q=</option>
|
||||
<option value='Erai-raws [Multi-Sub]'>https://nyaa.si/?page=rss&c=0_0&f=0&u=Erai-raws&q=</option>
|
||||
<option value='NanDesuKa'>https://nyaa.si/?page=rss&c=0_0&f=0&u=NanDesuKa&q=</option>
|
||||
</datalist>
|
||||
<div class='input-group-append'>
|
||||
<button type='button' on:click={() => { settings.rssFeeds.splice(i, 1); settings.rssFeeds = settings.rssFeeds }} class='btn btn-danger btn-lg input-group-append'>Remove</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue