mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-21 00:22:08 +00:00
Merge branch 'master' of https://github.com/RockinChaos/miru
This commit is contained in:
commit
859c894d6a
38 changed files with 1207 additions and 405 deletions
|
|
@ -30,6 +30,7 @@
|
|||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/browser": "^6.0.1",
|
||||
"@capacitor/core": "^6.1.1",
|
||||
"@capacitor/device": "^6.0.1",
|
||||
"@capacitor/ios": "^6.1.1",
|
||||
"@capacitor/local-notifications": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { SafeArea } from 'capacitor-plugin-safe-area'
|
|||
import { App } from '@capacitor/app'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Device } from '@capacitor/device'
|
||||
import IPC from './ipc.js'
|
||||
|
||||
IPC.on('open', url => Browser.open({ url }))
|
||||
|
|
@ -40,6 +41,16 @@ IPC.on('notification', noti => {
|
|||
if (canShowNotifications) LocalNotifications.schedule({ notifications: [notification] })
|
||||
})
|
||||
|
||||
IPC.on('get-device-info', async () => {
|
||||
const deviceInfo = {
|
||||
features: {},
|
||||
info: await Device.getInfo(),
|
||||
cpu: {},
|
||||
ram: {}
|
||||
}
|
||||
IPC.emit('device-info', JSON.stringify(deviceInfo))
|
||||
})
|
||||
|
||||
// schema: miru://key/value
|
||||
const protocolMap = {
|
||||
auth: token => sendToken(token),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
import { getContext } from 'svelte'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
|
|
@ -9,6 +10,13 @@
|
|||
$view = null
|
||||
page = 'home'
|
||||
}
|
||||
|
||||
const debug = persisted('debug', '', {
|
||||
serializer: {
|
||||
parse: e => e,
|
||||
stringify: e => e
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class='w-full z-101 navbar bg-transparent border-0 p-0 d-flex'>
|
||||
|
|
@ -27,8 +35,21 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $debug}
|
||||
<div class='ribbon right z-101 text-center position-fixed font-size-16 font-weight-bold'>Debug Mode!</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ribbon {
|
||||
background: #f63220;
|
||||
box-shadow: 0 0 0 999px #f63220;
|
||||
clip-path: inset(0 -100%);
|
||||
pointer-events: none;
|
||||
min-width: 120px;
|
||||
inset: 0 auto auto 0;
|
||||
transform-origin: 100% 0;
|
||||
transform: translate(-29.3%) rotate(-45deg);
|
||||
}
|
||||
.navbar {
|
||||
--navbar-height: 28px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,58 +3,9 @@
|
|||
import { media } from '../views/Player/MediaHandler.svelte'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import NavbarLink from './NavbarLink.svelte'
|
||||
const view = getContext('view')
|
||||
export let page
|
||||
const links = [
|
||||
{
|
||||
click: () => {
|
||||
page = 'search'
|
||||
},
|
||||
css: 'ml-auto',
|
||||
page: 'search',
|
||||
icon: 'search',
|
||||
text: 'Search'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'schedule'
|
||||
},
|
||||
page: 'schedule',
|
||||
icon: 'schedule',
|
||||
text: 'Schedule'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
if ($media) $view = $media.media
|
||||
},
|
||||
icon: 'queue_music',
|
||||
text: 'Now Playing'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'watchtogether'
|
||||
},
|
||||
page: 'watchtogether',
|
||||
icon: 'groups',
|
||||
text: 'Watch Together'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
|
||||
},
|
||||
icon: 'favorite',
|
||||
text: 'Support This App',
|
||||
css: 'ml-auto donate'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'settings'
|
||||
},
|
||||
page: 'settings',
|
||||
icon: 'settings',
|
||||
text: 'Settings'
|
||||
}
|
||||
]
|
||||
function close () {
|
||||
$view = null
|
||||
page = 'home'
|
||||
|
|
@ -64,99 +15,13 @@
|
|||
<nav class='navbar navbar-fixed-bottom d-block d-md-none border-0 bg-dark'>
|
||||
<div class='navbar-menu h-full d-flex flex-row justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
|
||||
<img src='./logo_filled.png' class='w-50 h-50 m-10 pointer p-5' alt='ico' use:click={close} />
|
||||
{#each links as { click: _click, icon, text, image, css, page: _page }, i (i)}
|
||||
<div
|
||||
class='navbar-link navbar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={_click}>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<NavbarLink click={() => { page = 'search' }} _page='search' css='ml-auto' icon='search' {page} />
|
||||
<NavbarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' {page} />
|
||||
{#if $media?.media}
|
||||
<NavbarLink click={() => { $view = $media.media }} icon='queue_music' {page} />
|
||||
{/if}
|
||||
<NavbarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' {page} />
|
||||
<NavbarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' css='ml-auto donate' {page} />
|
||||
<NavbarLink click={() => { page = 'settings' }} _page='settings' icon='settings' {page} />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
height: var(--navbar-height);
|
||||
}
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
/* .sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
} */
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link > span {
|
||||
color: #fff;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.navbar-link:hover > span {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
font-size: 1.4rem;
|
||||
padding: 0.75rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
101
common/components/NavbarLink.svelte
Normal file
101
common/components/NavbarLink.svelte
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
|
||||
let _click = () => {}
|
||||
export { _click as click }
|
||||
export let image = ''
|
||||
export let page
|
||||
export let _page = ''
|
||||
export let css = ''
|
||||
export let icon = ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
class='navbar-link navbar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={_click}>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
/* .sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
} */
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link > span {
|
||||
color: #fff;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.navbar-link:hover > span {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
font-size: 1.4rem;
|
||||
padding: 0.75rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,136 +3,43 @@
|
|||
import { media } from '@/views/Player/MediaHandler.svelte'
|
||||
import { settings } from '@/modules/settings.js'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { click } from '@/modules/click.js'
|
||||
import { profileView } from './Profiles.svelte'
|
||||
import Helper from '@/modules/helper.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import SidebarLink from './SidebarLink.svelte'
|
||||
|
||||
let wasUpdated = false
|
||||
|
||||
globalThis.dd = IPC
|
||||
let updateState = ''
|
||||
|
||||
IPC.on('update-available', () => {
|
||||
console.log('uwu')
|
||||
if (!wasUpdated) {
|
||||
// insert icon in 2nd to last position
|
||||
links.splice(links.length - 1, 0, {
|
||||
click: () => {
|
||||
toast('Update is downloading...')
|
||||
},
|
||||
icon: 'download',
|
||||
text: 'Update Downloading...'
|
||||
})
|
||||
links = links
|
||||
}
|
||||
wasUpdated = true
|
||||
updateState = 'downloading'
|
||||
})
|
||||
IPC.on('update-downloaded', () => {
|
||||
links[links.length - 2].css = 'update'
|
||||
links[links.length - 2].text = 'Update Ready!'
|
||||
links[links.length - 2].click = () => {
|
||||
IPC.emit('quit-and-install')
|
||||
}
|
||||
links = links
|
||||
updateState = 'ready'
|
||||
})
|
||||
|
||||
const view = getContext('view')
|
||||
|
||||
export let page
|
||||
|
||||
let links = [
|
||||
{
|
||||
click: () => {
|
||||
$profileView = true
|
||||
},
|
||||
icon: 'login',
|
||||
text: 'Login',
|
||||
css: 'mt-auto'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'home'
|
||||
if ($view) $view = null
|
||||
},
|
||||
page: 'home',
|
||||
icon: 'home',
|
||||
text: 'Home'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'search'
|
||||
if ($view) $view = null
|
||||
},
|
||||
page: 'search',
|
||||
icon: 'search',
|
||||
text: 'Search'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'schedule'
|
||||
},
|
||||
page: 'schedule',
|
||||
icon: 'schedule',
|
||||
text: 'Schedule'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
if ($media) $view = $media.media
|
||||
},
|
||||
icon: 'queue_music',
|
||||
text: 'Now Playing'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'watchtogether'
|
||||
},
|
||||
page: 'watchtogether',
|
||||
icon: 'groups',
|
||||
text: 'Watch Together'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
|
||||
},
|
||||
icon: 'favorite',
|
||||
text: 'Support This App',
|
||||
css: 'mt-auto donate'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'settings'
|
||||
},
|
||||
page: 'settings',
|
||||
icon: 'settings',
|
||||
text: 'Settings'
|
||||
}
|
||||
]
|
||||
if (Helper.getUser()) {
|
||||
links[0].image = Helper.getUserAvatar()
|
||||
links[0].text = 'Profiles'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='sidebar z-30 d-md-block' class:animated={$settings.expandingSidebar}>
|
||||
<div class='sidebar-overlay pointer-events-none h-full position-absolute' />
|
||||
<div class='sidebar-menu h-full d-flex flex-column justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
|
||||
{#each links as { click: _click, icon, text, image, css, page: _page } (_click)}
|
||||
<div
|
||||
class='sidebar-link sidebar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={() => { _click(); if (!icon.includes("login") && !icon.includes("favorite")) { window.dispatchEvent(new Event('overlay-check')) } } }>
|
||||
<span class='text-nowrap d-flex align-items-center w-full h-full'>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
<SidebarLink click={() => { $profileView = true }} icon='login' text={Helper.getUser() ? 'Profiles' : 'Login'} css='mt-auto' {page} image={Helper.getUserAvatar()} />
|
||||
<SidebarLink click={() => { page = 'home'; if ($view) $view = null }} _page='home' icon='home' text='Home' {page} />
|
||||
<SidebarLink click={() => { page = 'search'; if ($view) $view = null }} _page='search' icon='search' text='Search' {page} />
|
||||
<SidebarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' text='Schedule' {page} />
|
||||
{#if $media?.media}
|
||||
<SidebarLink click={() => { $view = $media.media }} icon='queue_music' text='Now Playing' {page} />
|
||||
{/if}
|
||||
<SidebarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' text='Watch Together' {page} />
|
||||
<SidebarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' text='Support This App' css='mt-auto donate' {page} />
|
||||
{#if updateState === 'downloading'}
|
||||
<SidebarLink click={() => { toast('Update is downloading...') }} icon='download' text='Update Downloading...' {page} />
|
||||
{:else if updateState === 'ready'}
|
||||
<SidebarLink click={() => { IPC.emit('quit-and-install') }} css='update' icon='download' text='Update Ready!' {page} />
|
||||
{/if}
|
||||
<SidebarLink click={() => { page = 'settings' }} _page='settings' icon='settings' text='Settings' {page} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
129
common/components/SidebarLink.svelte
Normal file
129
common/components/SidebarLink.svelte
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
|
||||
let _click = () => {}
|
||||
export { _click as click }
|
||||
export let image = ''
|
||||
export let page
|
||||
export let _page = ''
|
||||
export let css = ''
|
||||
export let text = ''
|
||||
export let icon = ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
class='sidebar-link sidebar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={_click}>
|
||||
<span class='text-nowrap d-flex align-items-center w-full h-full'>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
.update .material-symbols-outlined {
|
||||
color: #47cb6a;
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
.sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
}
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-link > span {
|
||||
color: #fff;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.sidebar-link:hover > span > *:nth-child(1) {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
width: 100%;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
background: none !important;
|
||||
overflow-y: unset;
|
||||
overflow-x: visible;
|
||||
left: unset;
|
||||
}
|
||||
.sidebar.animated:hover {
|
||||
width: 22rem
|
||||
}
|
||||
.sidebar-overlay {
|
||||
width: var(--sidebar-width);
|
||||
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
background: var(--sidebar-gradient);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -92,11 +92,9 @@
|
|||
use:click={() => playMedia(current)}>
|
||||
Watch Now
|
||||
</button>
|
||||
{#if Helper.isAniAuth()}
|
||||
<button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={current.isFavourite} use:click={toggleFavourite}>
|
||||
favorite
|
||||
</button>
|
||||
{/if}
|
||||
<button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={current.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
</button>
|
||||
<Scoring media={current} />
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
|
|
|
|||
|
|
@ -86,11 +86,9 @@
|
|||
</span>
|
||||
{playButtonText}
|
||||
</button>
|
||||
{#if Helper.isAniAuth()}
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite}>
|
||||
favorite
|
||||
</button>
|
||||
{/if}
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
</button>
|
||||
<Scoring {media} previewAnime={true}/>
|
||||
</div>
|
||||
<div class='details text-white text-capitalize pt-15 d-flex'>
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ body {
|
|||
background: #000;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.3 !important;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
|
@ -157,6 +161,10 @@ img[src=''], img[src=' '] {
|
|||
box-shadow: inset 0 0 0 1.5px #eee !important;
|
||||
}
|
||||
|
||||
.btn-secondary:focus-visible {
|
||||
box-shadow: inset 0 0 0 1.5px #000 !important;
|
||||
}
|
||||
|
||||
.modal:focus-visible {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { toast } from 'svelte-sonner'
|
|||
import { sleep } from './util.js'
|
||||
import Helper from '@/modules/helper.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:anilist')
|
||||
|
||||
export const codes = {
|
||||
400: 'Bad Request',
|
||||
|
|
@ -35,7 +38,7 @@ export const codes = {
|
|||
}
|
||||
|
||||
function printError (error) {
|
||||
console.warn(error)
|
||||
debug(`Error: ${error.status || 429} - ${error.message || codes[error.status || 429]}`)
|
||||
toast.error('Search Failed', {
|
||||
description: `Failed making request to anilist!\nTry again in a minute.\n${error.status || 429} - ${error.message || codes[error.status || 429]}`,
|
||||
duration: 3000
|
||||
|
|
@ -175,6 +178,7 @@ class AnilistClient {
|
|||
lastNotificationDate = Date.now() / 1000
|
||||
|
||||
constructor () {
|
||||
debug('Initializing Anilist Client for ID ' + this.userID?.viewer?.data?.Viewer.id)
|
||||
this.limiter.on('failed', async (error, jobInfo) => {
|
||||
printError(error)
|
||||
|
||||
|
|
@ -266,10 +270,12 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async findNewNotifications () {
|
||||
debug('Checking for new notifications')
|
||||
const res = await this.getNotifications()
|
||||
const notifications = res.data.Page.notifications
|
||||
const newNotifications = notifications.filter(({ createdAt }) => createdAt > this.lastNotificationDate)
|
||||
this.lastNotificationDate = Date.now() / 1000
|
||||
debug(`Found ${newNotifications.length} new notifications`)
|
||||
for (const { media, episode, type } of newNotifications) {
|
||||
const options = {
|
||||
title: media.title.userPreferred,
|
||||
|
|
@ -285,6 +291,7 @@ class AnilistClient {
|
|||
* @param {{key: string, title: string, year?: string, isAdult: boolean}[]} flattenedTitles
|
||||
**/
|
||||
async alSearchCompound (flattenedTitles) {
|
||||
debug(`Searching for ${flattenedTitles.length} titles via compound search`)
|
||||
if (!flattenedTitles.length) return []
|
||||
// isAdult doesn't need an extra variable, as the title is the same regardless of type, so we re-use the same variable for adult and non-adult requests
|
||||
/** @type {Record<`v${number}`, string>} */
|
||||
|
|
@ -349,6 +356,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async searchName (variables = {}) {
|
||||
debug(`Searching name for ${variables.name}`)
|
||||
const query = /* js */`
|
||||
query($page: Int, $perPage: Int, $sort: [MediaSort], $name: String, $status: [MediaStatus], $year: Int, $isAdult: Boolean) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
|
|
@ -371,6 +379,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async searchIDSingle (variables) {
|
||||
debug(`Searching for ID: ${variables.id}`)
|
||||
const query = /* js */`
|
||||
query($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
|
|
@ -387,6 +396,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async searchIDS (variables) {
|
||||
debug(`Searching for IDs: ${variables.id.length}`)
|
||||
const query = /* js */`
|
||||
query($id: [Int], $idMal: [Int], $id_not: [Int], $page: Int, $perPage: Int, $status: [MediaStatus], $onList: Boolean, $sort: [MediaSort], $search: String, $season: MediaSeason, $year: Int, $genre: [String], $tag: [String], $format: MediaFormat) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
|
|
@ -409,6 +419,7 @@ class AnilistClient {
|
|||
|
||||
/** @returns {Promise<import('./al.d.ts').PagedQuery<{ notifications: { id: number, type: string, createdAt: number, episode: number, media: import('./al.d.ts').Media}[] }>>} */
|
||||
getNotifications (variables = {}) {
|
||||
debug('Getting notifications')
|
||||
const query = /* js */`
|
||||
query($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
|
|
@ -451,6 +462,7 @@ class AnilistClient {
|
|||
|
||||
/** @returns {Promise<import('./al.d.ts').Query<{ Viewer: import('./al.d.ts').Viewer }>>} */
|
||||
viewer (variables = {}) {
|
||||
debug('Getting viewer')
|
||||
const query = /* js */`
|
||||
query {
|
||||
Viewer {
|
||||
|
|
@ -471,7 +483,8 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
/** @returns {Promise<import('./al.d.ts').Query<{ MediaListCollection: import('./al.d.ts').MediaListCollection }>>} */
|
||||
async getUserLists (variables) {
|
||||
async getUserLists (variables = {}) {
|
||||
debug('Getting user lists')
|
||||
variables.id = !variables.userID ? this.userID?.viewer?.data?.Viewer.id : variables.userID
|
||||
variables.sort = variables.sort?.replace('USER_SCORE_DESC', 'SCORE_DESC') || 'UPDATED_TIME_DESC' // doesn't exist, AniList uses SCORE_DESC for both MediaSort and MediaListSort.
|
||||
const query = /* js */`
|
||||
|
|
@ -496,6 +509,7 @@ class AnilistClient {
|
|||
/** @returns {Promise<import('./al.d.ts').Query<{ MediaList: { status: string, progress: number, repeat: number }}>>} */
|
||||
async searchIDStatus (variables = {}) {
|
||||
variables.id = this.userID?.viewer?.data?.Viewer.id
|
||||
debug(`Searching for ID status: ${variables.id}`)
|
||||
const query = /* js */`
|
||||
query($id: Int, $mediaId: Int) {
|
||||
MediaList(userId: $id, mediaId: $mediaId) {
|
||||
|
|
@ -509,6 +523,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async searchAiringSchedule (variables = {}) {
|
||||
debug('Searching for airing schedule')
|
||||
variables.to = (variables.from + 7 * 24 * 60 * 60)
|
||||
const query = /* js */`
|
||||
query($page: Int, $perPage: Int, $from: Int, $to: Int) {
|
||||
|
|
@ -536,7 +551,8 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
/** @returns {Promise<import('./al.d.ts').PagedQuery<{ airingSchedules: { airingAt: number, episode: number }[]}>>} */
|
||||
episodes (variables) {
|
||||
episodes (variables = {}) {
|
||||
debug(`Searching for episodes: ${variables.id}`)
|
||||
const query = /* js */`
|
||||
query($id: Int) {
|
||||
Page(page: 1, perPage: 1000) {
|
||||
|
|
@ -551,6 +567,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async search (variables = {}) {
|
||||
debug(`Searching ${JSON.stringify(variables)}`)
|
||||
variables.sort ||= 'SEARCH_MATCH'
|
||||
const query = /* js */`
|
||||
query($page: Int, $perPage: Int, $sort: [MediaSort], $search: String, $onList: Boolean, $status: MediaStatus, $status_not: MediaStatus, $season: MediaSeason, $year: Int, $genre: [String], $tag: [String], $format: MediaFormat, $id_not: [Int], $idMal_not: [Int], $idMal: [Int]) {
|
||||
|
|
@ -574,6 +591,7 @@ class AnilistClient {
|
|||
|
||||
/** @returns {Promise<import('./al.d.ts').Query<{ AiringSchedule: { airingAt: number }}>>} */
|
||||
episodeDate (variables) {
|
||||
debug(`Searching for episode date: ${variables.id}, ${variables.ep}`)
|
||||
const query = /* js */`
|
||||
query($id: Int, $ep: Int) {
|
||||
AiringSchedule(mediaId: $id, episode: $ep) {
|
||||
|
|
@ -586,6 +604,7 @@ class AnilistClient {
|
|||
|
||||
/** @returns {Promise<import('./al.d.ts').PagedQuery<{ mediaList: import('./al.d.ts').Following[]}>>} */
|
||||
following (variables) {
|
||||
debug('Getting following')
|
||||
const query = /* js */`
|
||||
query($id: Int) {
|
||||
Page {
|
||||
|
|
@ -607,6 +626,7 @@ class AnilistClient {
|
|||
|
||||
/** @returns {Promise<import('./al.d.ts').Query<{Media: import('./al.d.ts').Media}>>} */
|
||||
recommendations (variables) {
|
||||
debug(`Getting recommendations for ${variables.id}`)
|
||||
const query = /* js */`
|
||||
query($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
|
|
@ -634,6 +654,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async entry (variables) {
|
||||
debug(`Updating entry for ${variables.id}`)
|
||||
const query = /* js */`
|
||||
mutation($lists: [String], $id: Int, $status: MediaListStatus, $episode: Int, $repeat: Int, $score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput) {
|
||||
SaveMediaListEntry(mediaId: $id, status: $status, progress: $episode, repeat: $repeat, scoreRaw: $score, customLists: $lists, startedAt: $startedAt, completedAt: $completedAt) {
|
||||
|
|
@ -661,6 +682,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
async delete (variables) {
|
||||
debug(`Deleting entry for ${variables.id}`)
|
||||
const query = /* js */`
|
||||
mutation($id: Int) {
|
||||
DeleteMediaListEntry(id: $id) {
|
||||
|
|
@ -673,6 +695,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
favourite (variables) {
|
||||
debug(`Toggling favourite for ${variables.id}`)
|
||||
const query = /* js */`
|
||||
mutation($id: Int) {
|
||||
ToggleFavourite(animeId: $id) { anime { nodes { id } } }
|
||||
|
|
@ -682,6 +705,7 @@ class AnilistClient {
|
|||
}
|
||||
|
||||
customList (variables = {}) {
|
||||
debug('Updating custom list')
|
||||
variables.lists = [...variables.lists, 'Watched using Miru']
|
||||
const query = /* js */`
|
||||
mutation($lists: [String]) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { anilistClient } from './anilist.js'
|
||||
import { anitomyscript } from './anime.js'
|
||||
import { chunks } from './util.js'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:animeresolver')
|
||||
|
||||
const postfix = {
|
||||
1: 'st', 2: 'nd', 3: 'rd'
|
||||
|
|
@ -73,8 +76,11 @@ export default new class AnimeResolver {
|
|||
return titleObjects
|
||||
}).flat()
|
||||
|
||||
debug(`Finding ${titleObjects.length} titles: ${titleObjects.map(obj => obj.title).join(', ')}`)
|
||||
|
||||
for (const chunk of chunks(titleObjects, 62)) { // single title has a complexity of 8.1, al limits complexity to 500
|
||||
for (const [key, media] of await anilistClient.alSearchCompound(chunk)) {
|
||||
debug(`Found ${key} as ${media.id}: ${media.title.userPreferred}`)
|
||||
this.animeNameCache[key] = media
|
||||
}
|
||||
}
|
||||
|
|
@ -118,10 +124,12 @@ export default new class AnimeResolver {
|
|||
let media = this.animeNameCache[this.getCacheKeyForTitle(parseObj)]
|
||||
// resolve episode, if movie, dont.
|
||||
const maxep = media?.nextAiringEpisode?.episode || media?.episodes
|
||||
debug(`Resolving ${parseObj.anime_title} ${parseObj.episode_number} ${maxep} ${media?.title.userPreferred} ${media?.format}`)
|
||||
if ((media?.format !== 'MOVIE' || maxep) && parseObj.episode_number) {
|
||||
if (Array.isArray(parseObj.episode_number)) {
|
||||
// is an episode range
|
||||
if (parseInt(parseObj.episode_number[0]) === 1) {
|
||||
debug('Range starts at 1')
|
||||
// if it starts with #1 and overflows then it includes more than 1 season in a batch, cant fix this cleanly, name is parsed per file basis so this shouldnt be an issue
|
||||
episode = `${parseObj.episode_number[0]} ~ ${parseObj.episode_number[1]}`
|
||||
} else {
|
||||
|
|
@ -132,7 +140,9 @@ export default new class AnimeResolver {
|
|||
// parent check is to break out of those incorrectly resolved OVA's
|
||||
// if we used anime season to resolve anime name, then there's no need to march into prequel!
|
||||
const prequel = !parseObj.anime_season && (this.findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && this.findEdge(media, 'PARENT')?.node))
|
||||
debug(`Prequel ${prequel && prequel.id}:${prequel && prequel.title.userPreferred}`)
|
||||
const root = prequel && (await this.resolveSeason({ media: await this.getAnimeById(prequel.id), force: true })).media
|
||||
debug(`Root ${root && root.id}:${root && root.title.userPreferred}`)
|
||||
|
||||
// if highest value is bigger than episode count or latest streamed episode +1 for safety, parseint to math.floor a number like 12.5 - specials - in 1 go
|
||||
let result = await this.resolveSeason({ media: root || media, episode: parseObj.episode_number[1], increment: !parseObj.anime_season ? null : true })
|
||||
|
|
@ -142,10 +152,12 @@ export default new class AnimeResolver {
|
|||
result = await this.resolveSeason({ media: root || media, episode: parseObj.episode_number[1] })
|
||||
}
|
||||
|
||||
debug(`Found rootMedia for ${parseObj.anime_title}: ${result.rootMedia.id}:${result.rootMedia.title.userPreferred} from ${media.id}:${media.title.userPreferred}`)
|
||||
media = result.rootMedia
|
||||
const diff = parseObj.episode_number[1] - result.episode
|
||||
episode = `${parseObj.episode_number[0] - diff} ~ ${result.episode}`
|
||||
failed = result.failed
|
||||
if (failed) debug(`Failed to resolve ${parseObj.anime_title} ${parseObj.episode_number} ${media?.title.userPreferred}`)
|
||||
} else {
|
||||
// cant find ep count or range seems fine
|
||||
episode = `${Number(parseObj.episode_number[0])} ~ ${Number(parseObj.episode_number[1])}`
|
||||
|
|
@ -155,7 +167,9 @@ export default new class AnimeResolver {
|
|||
if (maxep && parseInt(parseObj.episode_number) > maxep) {
|
||||
// see big comment above
|
||||
const prequel = !parseObj.anime_season && (this.findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && this.findEdge(media, 'PARENT')?.node))
|
||||
debug(`Prequel ${prequel && prequel.id}:${prequel && prequel.title.userPreferred}`)
|
||||
const root = prequel && (await this.resolveSeason({ media: await this.getAnimeById(prequel.id), force: true })).media
|
||||
debug(`Root ${root && root.id}:${root && root.title.userPreferred}`)
|
||||
|
||||
// value bigger than episode count
|
||||
let result = await this.resolveSeason({ media: root || media, episode: parseInt(parseObj.episode_number), increment: !parseObj.anime_season ? null : true })
|
||||
|
|
@ -165,15 +179,18 @@ export default new class AnimeResolver {
|
|||
result = await this.resolveSeason({ media: root || media, episode: parseInt(parseObj.episode_number) })
|
||||
}
|
||||
|
||||
debug(`Found rootMedia for ${parseObj.anime_title}: ${result.rootMedia.id}:${result.rootMedia.title.userPreferred} from ${media.id}:${media.title.userPreferred}`)
|
||||
media = result.rootMedia
|
||||
episode = result.episode
|
||||
failed = result.failed
|
||||
if (failed) debug(`Failed to resolve ${parseObj.anime_title} ${parseObj.episode_number} ${media?.title.userPreferred}`)
|
||||
} else {
|
||||
// cant find ep count or episode seems fine
|
||||
episode = Number(parseObj.episode_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
debug(`Resolved ${parseObj.anime_title} ${parseObj.episode_number} ${episode} ${media?.id}:${media?.title.userPreferred}`)
|
||||
fileAnimes.push({
|
||||
episode: episode || parseObj.episode_number,
|
||||
parseObject: parseObj,
|
||||
|
|
@ -184,6 +201,12 @@ export default new class AnimeResolver {
|
|||
return fileAnimes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./al.js').Media} media
|
||||
* @param {string} type
|
||||
* @param {string[]} [formats]
|
||||
* @param {boolean} [skip]
|
||||
*/
|
||||
findEdge (media, type, formats = ['TV', 'TV_SHORT'], skip) {
|
||||
let res = media.relations.edges.find(edge => {
|
||||
if (edge.relationType === type) {
|
||||
|
|
@ -198,9 +221,8 @@ export default new class AnimeResolver {
|
|||
|
||||
// note: this doesnt cover anime which uses partially relative and partially absolute episode number, BUT IT COULD!
|
||||
/**
|
||||
*
|
||||
* @param {{ media:any, episode?:number, force?:boolean, increment?:boolean, offset?: number, rootMedia?:any }} opts
|
||||
* @returns
|
||||
* @param {{ media: import('./al.js').Media , episode?:number, force?:boolean, increment?:boolean, offset?: number, rootMedia?: import('./al.js').Media }} opts
|
||||
* @returns {Promise<{ media: import('./al.js').Media, episode: number, offset: number, increment: boolean, rootMedia: import('./al.js').Media, failed?: boolean }>}
|
||||
*/
|
||||
async resolveSeason (opts) {
|
||||
// media, episode, increment, offset, force
|
||||
|
|
@ -217,7 +239,7 @@ export default new class AnimeResolver {
|
|||
|
||||
if (!edge) {
|
||||
const obj = { media, episode: episode - offset, offset, increment, rootMedia, failed: true }
|
||||
if (!force) console.warn('Error in parsing!', obj)
|
||||
if (!force) debug(`Failed to resolve ${media.id}:${media.title.userPreferred} ${episode} ${increment} ${offset} ${rootMedia.id}:${rootMedia.title.userPreferred}`)
|
||||
return obj
|
||||
}
|
||||
media = await this.getAnimeById(edge.id)
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ const DirectionKeyMap = { ArrowDown: 'down', ArrowUp: 'up', ArrowLeft: 'left', A
|
|||
* @returns {number} - The direction between the two points.
|
||||
*/
|
||||
function getDirection (anchor, relative) {
|
||||
return Math.round((Math.atan2(relative.y - anchor.y, relative.x - anchor.x) * 180 / Math.PI + 180) / 90)
|
||||
return Math.round((Math.atan2(relative.y - anchor.y, relative.x - anchor.x) * 180 / Math.PI + 180) / 90) || 4
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,7 +143,7 @@ function getDistance (anchor, relative) {
|
|||
* @returns {Element[]} - An array of keyboard-focusable elements.
|
||||
*/
|
||||
function getKeyboardFocusableElements (element = document.body) {
|
||||
return [...element.querySelectorAll('a[href], button:not([disabled]), fieldset:not([disabled]), input:not([disabled]), optgroup:not([disabled]), option:not([disabled]), select:not([disabled]), textarea:not([disabled]), details, [tabindex]:not([tabindex="-1"]), [contenteditable], [controls]')].filter(
|
||||
return [...element.querySelectorAll('a[href], button:not([disabled]), fieldset:not([disabled]), input:not([disabled]), optgroup:not([disabled]), option:not([disabled]), select:not([disabled]), textarea:not([disabled]), details, [tabindex]:not([tabindex="-1"], [disabled]), [contenteditable], [controls]')].filter(
|
||||
el => !el.getAttribute('aria-hidden')
|
||||
)
|
||||
}
|
||||
|
|
|
|||
545
common/modules/debug.js
Normal file
545
common/modules/debug.js
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
/* eslint-env browser */
|
||||
// patched version of debug because there's actually not a way to disable colors globally!
|
||||
|
||||
/**
|
||||
* This is the web browser implementation of `debug()`.
|
||||
*/
|
||||
|
||||
exports.formatArgs = formatArgs
|
||||
exports.save = save
|
||||
exports.load = load
|
||||
exports.useColors = useColors
|
||||
exports.storage = localstorage()
|
||||
exports.destroy = (() => {
|
||||
let warned = false
|
||||
|
||||
return () => {
|
||||
if (!warned) {
|
||||
warned = true
|
||||
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.')
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
/**
|
||||
* Colors.
|
||||
*/
|
||||
|
||||
exports.colors = [
|
||||
'#0000CC',
|
||||
'#0000FF',
|
||||
'#0033CC',
|
||||
'#0033FF',
|
||||
'#0066CC',
|
||||
'#0066FF',
|
||||
'#0099CC',
|
||||
'#0099FF',
|
||||
'#00CC00',
|
||||
'#00CC33',
|
||||
'#00CC66',
|
||||
'#00CC99',
|
||||
'#00CCCC',
|
||||
'#00CCFF',
|
||||
'#3300CC',
|
||||
'#3300FF',
|
||||
'#3333CC',
|
||||
'#3333FF',
|
||||
'#3366CC',
|
||||
'#3366FF',
|
||||
'#3399CC',
|
||||
'#3399FF',
|
||||
'#33CC00',
|
||||
'#33CC33',
|
||||
'#33CC66',
|
||||
'#33CC99',
|
||||
'#33CCCC',
|
||||
'#33CCFF',
|
||||
'#6600CC',
|
||||
'#6600FF',
|
||||
'#6633CC',
|
||||
'#6633FF',
|
||||
'#66CC00',
|
||||
'#66CC33',
|
||||
'#9900CC',
|
||||
'#9900FF',
|
||||
'#9933CC',
|
||||
'#9933FF',
|
||||
'#99CC00',
|
||||
'#99CC33',
|
||||
'#CC0000',
|
||||
'#CC0033',
|
||||
'#CC0066',
|
||||
'#CC0099',
|
||||
'#CC00CC',
|
||||
'#CC00FF',
|
||||
'#CC3300',
|
||||
'#CC3333',
|
||||
'#CC3366',
|
||||
'#CC3399',
|
||||
'#CC33CC',
|
||||
'#CC33FF',
|
||||
'#CC6600',
|
||||
'#CC6633',
|
||||
'#CC9900',
|
||||
'#CC9933',
|
||||
'#CCCC00',
|
||||
'#CCCC33',
|
||||
'#FF0000',
|
||||
'#FF0033',
|
||||
'#FF0066',
|
||||
'#FF0099',
|
||||
'#FF00CC',
|
||||
'#FF00FF',
|
||||
'#FF3300',
|
||||
'#FF3333',
|
||||
'#FF3366',
|
||||
'#FF3399',
|
||||
'#FF33CC',
|
||||
'#FF33FF',
|
||||
'#FF6600',
|
||||
'#FF6633',
|
||||
'#FF9900',
|
||||
'#FF9933',
|
||||
'#FFCC00',
|
||||
'#FFCC33'
|
||||
]
|
||||
|
||||
const { formatters = {} } = module.exports
|
||||
|
||||
/**
|
||||
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
|
||||
* and the Firebug extension (any Firefox version) are known
|
||||
* to support "%c" CSS customizations.
|
||||
*
|
||||
* TODO: add a `localStorage` variable to explicitly enable/disable colors
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function useColors () {
|
||||
return false
|
||||
// NB: In an Electron preload script, document will be defined but not fully
|
||||
// initialized. Since we know we're in Chrome, we'll just detect this case
|
||||
// explicitly
|
||||
if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Internet Explorer and Edge do not support colors.
|
||||
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
let m
|
||||
|
||||
// Is webkit? http://stackoverflow.com/a/16459606/376773
|
||||
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
|
||||
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
|
||||
// Is firebug? http://stackoverflow.com/a/398120/376773
|
||||
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
|
||||
// Is firefox >= v31?
|
||||
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
|
||||
(typeof navigator !== 'undefined' && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31) ||
|
||||
// Double check webkit in userAgent just in case we are in a worker
|
||||
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorize log arguments if enabled.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function formatArgs (args) {
|
||||
args[0] = (this.useColors ? '%c' : '') +
|
||||
this.namespace +
|
||||
(this.useColors ? ' %c' : ' ') +
|
||||
args[0] +
|
||||
(this.useColors ? '%c ' : ' ') +
|
||||
'+' + module.exports.humanize(this.diff)
|
||||
|
||||
if (!this.useColors) {
|
||||
return
|
||||
}
|
||||
|
||||
const c = 'color: ' + this.color
|
||||
args.splice(1, 0, c, 'color: inherit')
|
||||
|
||||
// The final "%c" is somewhat tricky, because there could be other
|
||||
// arguments passed either before or after the %c, so we need to
|
||||
// figure out the correct index to insert the CSS into
|
||||
let index = 0
|
||||
let lastC = 0
|
||||
args[0].replace(/%[a-zA-Z%]/g, match => {
|
||||
if (match === '%%') {
|
||||
return
|
||||
}
|
||||
index++
|
||||
if (match === '%c') {
|
||||
// We only are interested in the *last* %c
|
||||
// (the user may have provided their own)
|
||||
lastC = index
|
||||
}
|
||||
})
|
||||
|
||||
args.splice(lastC, 0, c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes `console.debug()` when available.
|
||||
* No-op when `console.debug` is not a "function".
|
||||
* If `console.debug` is not available, falls back
|
||||
* to `console.log`.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
exports.log = console.debug || console.log || (() => {})
|
||||
|
||||
/**
|
||||
* Save `namespaces`.
|
||||
*
|
||||
* @param {String} namespaces
|
||||
* @api private
|
||||
*/
|
||||
function save (namespaces) {
|
||||
try {
|
||||
if (namespaces) {
|
||||
exports.storage.setItem('debug', namespaces)
|
||||
} else {
|
||||
exports.storage.removeItem('debug')
|
||||
}
|
||||
} catch (error) {
|
||||
// Swallow
|
||||
// XXX (@Qix-) should we be logging these?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load `namespaces`.
|
||||
*
|
||||
* @return {String} returns the previously persisted debug modes
|
||||
* @api private
|
||||
*/
|
||||
function load () {
|
||||
let r
|
||||
try {
|
||||
r = exports.storage.getItem('debug')
|
||||
} catch (error) {
|
||||
// Swallow
|
||||
// XXX (@Qix-) should we be logging these?
|
||||
}
|
||||
|
||||
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
|
||||
if (!r && typeof process !== 'undefined' && 'env' in process) {
|
||||
r = process.env.DEBUG
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Localstorage attempts to return the localstorage.
|
||||
*
|
||||
* This is necessary because safari throws
|
||||
* when a user disables cookies/localstorage
|
||||
* and you attempt to access it.
|
||||
*
|
||||
* @return {LocalStorage}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function localstorage () {
|
||||
try {
|
||||
// TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context
|
||||
// The Browser also has localStorage in the global context.
|
||||
return localStorage
|
||||
} catch (error) {
|
||||
// Swallow
|
||||
// XXX (@Qix-) should we be logging these?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
|
||||
*/
|
||||
|
||||
formatters.j = function (v) {
|
||||
try {
|
||||
return JSON.stringify(v)
|
||||
} catch (error) {
|
||||
return '[UnexpectedJSONParseError]: ' + error.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the common logic for both the Node.js and web browser
|
||||
* implementations of `debug()`.
|
||||
*/
|
||||
|
||||
function setup (env) {
|
||||
createDebug.debug = createDebug
|
||||
createDebug.default = createDebug
|
||||
createDebug.coerce = coerce
|
||||
createDebug.disable = disable
|
||||
createDebug.enable = enable
|
||||
createDebug.enabled = enabled
|
||||
createDebug.humanize = require('ms')
|
||||
createDebug.destroy = destroy
|
||||
|
||||
Object.keys(env).forEach(key => {
|
||||
createDebug[key] = env[key]
|
||||
})
|
||||
|
||||
/**
|
||||
* The currently active debug mode names, and names to skip.
|
||||
*/
|
||||
|
||||
createDebug.names = []
|
||||
createDebug.skips = []
|
||||
|
||||
/**
|
||||
* Map of special "%n" handling functions, for the debug "format" argument.
|
||||
*
|
||||
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
|
||||
*/
|
||||
createDebug.formatters = {}
|
||||
|
||||
/**
|
||||
* Selects a color for a debug namespace
|
||||
* @param {String} namespace The namespace string for the debug instance to be colored
|
||||
* @return {Number|String} An ANSI color code for the given namespace
|
||||
* @api private
|
||||
*/
|
||||
function selectColor (namespace) {
|
||||
let hash = 0
|
||||
|
||||
for (let i = 0; i < namespace.length; i++) {
|
||||
hash = ((hash << 5) - hash) + namespace.charCodeAt(i)
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
|
||||
return createDebug.colors[Math.abs(hash) % createDebug.colors.length]
|
||||
}
|
||||
createDebug.selectColor = selectColor
|
||||
|
||||
/**
|
||||
* Create a debugger with the given `namespace`.
|
||||
*
|
||||
* @param {String} namespace
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
function createDebug (namespace) {
|
||||
let prevTime
|
||||
let enableOverride = null
|
||||
let namespacesCache
|
||||
let enabledCache
|
||||
|
||||
function debug (...args) {
|
||||
// Disabled?
|
||||
if (!debug.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const self = debug
|
||||
|
||||
// Set `diff` timestamp
|
||||
const curr = Number(new Date())
|
||||
const ms = curr - (prevTime || curr)
|
||||
self.diff = ms
|
||||
self.prev = prevTime
|
||||
self.curr = curr
|
||||
prevTime = curr
|
||||
|
||||
args[0] = createDebug.coerce(args[0])
|
||||
|
||||
if (typeof args[0] !== 'string') {
|
||||
// Anything else let's inspect with %O
|
||||
args.unshift('%O')
|
||||
}
|
||||
|
||||
// Apply any `formatters` transformations
|
||||
let index = 0
|
||||
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
|
||||
// If we encounter an escaped % then don't increase the array index
|
||||
if (match === '%%') {
|
||||
return '%'
|
||||
}
|
||||
index++
|
||||
const formatter = createDebug.formatters[format]
|
||||
if (typeof formatter === 'function') {
|
||||
const val = args[index]
|
||||
match = formatter.call(self, val)
|
||||
|
||||
// Now we need to remove `args[index]` since it's inlined in the `format`
|
||||
args.splice(index, 1)
|
||||
index--
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
// Apply env-specific formatting (colors, etc.)
|
||||
createDebug.formatArgs.call(self, args)
|
||||
|
||||
const logFn = self.log || createDebug.log
|
||||
logFn.apply(self, args)
|
||||
}
|
||||
|
||||
debug.namespace = namespace
|
||||
debug.useColors = createDebug.useColors()
|
||||
debug.color = createDebug.selectColor(namespace)
|
||||
debug.extend = extend
|
||||
debug.destroy = createDebug.destroy // XXX Temporary. Will be removed in the next major release.
|
||||
|
||||
Object.defineProperty(debug, 'enabled', {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => {
|
||||
if (enableOverride !== null) {
|
||||
return enableOverride
|
||||
}
|
||||
if (namespacesCache !== createDebug.namespaces) {
|
||||
namespacesCache = createDebug.namespaces
|
||||
enabledCache = createDebug.enabled(namespace)
|
||||
}
|
||||
|
||||
return enabledCache
|
||||
},
|
||||
set: v => {
|
||||
enableOverride = v
|
||||
}
|
||||
})
|
||||
|
||||
// Env-specific initialization logic for debug instances
|
||||
if (typeof createDebug.init === 'function') {
|
||||
createDebug.init(debug)
|
||||
}
|
||||
|
||||
return debug
|
||||
}
|
||||
|
||||
function extend (namespace, delimiter) {
|
||||
const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace)
|
||||
newDebug.log = this.log
|
||||
return newDebug
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a debug mode by namespaces. This can include modes
|
||||
* separated by a colon and wildcards.
|
||||
*
|
||||
* @param {String} namespaces
|
||||
* @api public
|
||||
*/
|
||||
function enable (namespaces) {
|
||||
createDebug.save(namespaces)
|
||||
createDebug.namespaces = namespaces
|
||||
|
||||
createDebug.names = []
|
||||
createDebug.skips = []
|
||||
|
||||
let i
|
||||
const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/)
|
||||
const len = split.length
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!split[i]) {
|
||||
// ignore empty strings
|
||||
continue
|
||||
}
|
||||
|
||||
namespaces = split[i].replace(/\*/g, '.*?')
|
||||
|
||||
if (namespaces[0] === '-') {
|
||||
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'))
|
||||
} else {
|
||||
createDebug.names.push(new RegExp('^' + namespaces + '$'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable debug output.
|
||||
*
|
||||
* @return {String} namespaces
|
||||
* @api public
|
||||
*/
|
||||
function disable () {
|
||||
const namespaces = [
|
||||
...createDebug.names.map(toNamespace),
|
||||
...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
|
||||
].join(',')
|
||||
createDebug.enable('')
|
||||
return namespaces
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given mode name is enabled, false otherwise.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
function enabled (name) {
|
||||
if (name[name.length - 1] === '*') {
|
||||
return true
|
||||
}
|
||||
|
||||
let i
|
||||
let len
|
||||
|
||||
for (i = 0, len = createDebug.skips.length; i < len; i++) {
|
||||
if (createDebug.skips[i].test(name)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, len = createDebug.names.length; i < len; i++) {
|
||||
if (createDebug.names[i].test(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert regexp to namespace
|
||||
*
|
||||
* @param {RegExp} regxep
|
||||
* @return {String} namespace
|
||||
* @api private
|
||||
*/
|
||||
function toNamespace (regexp) {
|
||||
return regexp.toString()
|
||||
.substring(2, regexp.toString().length - 2)
|
||||
.replace(/\.\*\?$/, '*')
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce `val`.
|
||||
*
|
||||
* @param {Mixed} val
|
||||
* @return {Mixed}
|
||||
* @api private
|
||||
*/
|
||||
function coerce (val) {
|
||||
if (val instanceof Error) {
|
||||
return val.stack || val.message
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
/**
|
||||
* XXX DO NOT USE. This is a temporary stub function.
|
||||
* XXX It WILL be removed in the next major release.
|
||||
*/
|
||||
function destroy () {
|
||||
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.')
|
||||
}
|
||||
|
||||
createDebug.enable(createDebug.load())
|
||||
|
||||
return createDebug
|
||||
}
|
||||
|
||||
module.exports = setup(exports)
|
||||
|
|
@ -6,6 +6,9 @@ import { anitomyscript } from '../anime.js'
|
|||
import { client } from '@/modules/torrent.js'
|
||||
import { extensionsWorker } from '@/views/Settings/TorrentSettings.svelte'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:extensions')
|
||||
|
||||
/** @typedef {import('@thaunknown/ani-resourced/sources/types.d.ts').Options} Options */
|
||||
/** @typedef {import('@thaunknown/ani-resourced/sources/types.d.ts').Result} Result */
|
||||
|
|
@ -16,11 +19,17 @@ import { toast } from 'svelte-sonner'
|
|||
* **/
|
||||
export default async function getResultsFromExtensions ({ media, episode, batch, movie, resolution }) {
|
||||
const worker = await /** @type {ReturnType<import('@/modules/extensions/worker.js').loadExtensions>} */(extensionsWorker)
|
||||
if (!(await worker.metadata)?.length) throw new Error('No torrent sources configured. Add extensions in settings.')
|
||||
if (!(await worker.metadata)?.length) {
|
||||
debug('No torrent sources configured')
|
||||
throw new Error('No torrent sources configured. Add extensions in settings.')
|
||||
}
|
||||
|
||||
debug(`Fetching sources for ${media.id}:${media.title.userPreferred} ${episode} ${batch} ${movie} ${resolution}`)
|
||||
|
||||
const aniDBMeta = await ALToAniDB(media)
|
||||
const anidbAid = aniDBMeta?.mappings?.anidb_id
|
||||
const anidbEid = anidbAid && (await ALtoAniDBEpisode({ media, episode }, aniDBMeta))?.anidbEid
|
||||
debug(`AniDB Mapping: ${anidbAid} ${anidbEid}`)
|
||||
|
||||
/** @type {Options} */
|
||||
const options = {
|
||||
|
|
@ -36,8 +45,10 @@ export default async function getResultsFromExtensions ({ media, episode, batch,
|
|||
|
||||
const { results, errors } = await worker.query(options, { movie, batch }, settings.value.sources)
|
||||
|
||||
debug(`Found ${results.length} results`)
|
||||
|
||||
for (const error of errors) {
|
||||
console.error(error)
|
||||
debug(`Source Fetch Failed: ${error}`)
|
||||
toast.error('Source Fetch Failed!', {
|
||||
description: error
|
||||
})
|
||||
|
|
@ -56,11 +67,13 @@ export default async function getResultsFromExtensions ({ media, episode, batch,
|
|||
|
||||
async function updatePeerCounts (entries) {
|
||||
const id = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString()
|
||||
debug(`Updating peer counts for ${entries.length} entries`)
|
||||
|
||||
const updated = await Promise.race([
|
||||
new Promise(resolve => {
|
||||
function check ({ detail }) {
|
||||
if (detail.id !== id) return
|
||||
debug('Got scrape response')
|
||||
client.removeListener('scrape', check)
|
||||
resolve(detail.result)
|
||||
}
|
||||
|
|
@ -69,6 +82,7 @@ async function updatePeerCounts (entries) {
|
|||
}),
|
||||
sleep(15000)
|
||||
])
|
||||
debug('Scrape complete')
|
||||
|
||||
for (const { hash, complete, downloaded, incomplete } of updated || []) {
|
||||
const found = entries.find(mapped => mapped.hash === hash)
|
||||
|
|
@ -76,6 +90,8 @@ async function updatePeerCounts (entries) {
|
|||
found.leechers = incomplete
|
||||
found.seeders = complete
|
||||
}
|
||||
|
||||
debug(`Found ${(updated || []).length} entries: ${JSON.stringify(updated)}`)
|
||||
return entries
|
||||
}
|
||||
|
||||
|
|
@ -110,12 +126,18 @@ function getRelation (list, type) {
|
|||
* @param {{episodes: any, episodeCount: number, specialCount: number}} param1
|
||||
* */
|
||||
async function ALtoAniDBEpisode ({ media, episode }, { episodes, episodeCount, specialCount }) {
|
||||
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 && episodes[Number(episode)])) return episodes[Number(episode)]
|
||||
if (!specialCount || (media.episodes && media.episodes === episodeCount && episodes[Number(episode)])) {
|
||||
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 res = await anilistClient.episodeDate({ id: media.id, ep: episode })
|
||||
// TODO: if media only has one episode, and airdate doesn't exist use start/release/end dates
|
||||
const alDate = new Date((res.data.AiringSchedule?.airingAt || 0) * 1000)
|
||||
debug(`AL Airdate: ${alDate}`)
|
||||
|
||||
return episodeByAirDate(alDate, episodes, episode)
|
||||
}
|
||||
|
|
@ -126,6 +148,7 @@ async function ALtoAniDBEpisode ({ media, episode }, { episodes, episodeCount, s
|
|||
* @param {number} episode
|
||||
**/
|
||||
export function episodeByAirDate (alDate, episodes, episode) {
|
||||
// TODO handle special cases where anilist reports that 3 episodes aired at the same time because of pre-releases
|
||||
if (!+alDate) return episodes[Number(episode)] || episodes[1] // what the fuck, are you braindead anilist?, the source episode number to play is from an array created from AL ep count, so how come it's missing?
|
||||
// 1 is key for episod 1, not index
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import Metadata from 'matroska-metadata'
|
||||
import Debug from 'debug'
|
||||
import { arr2hex, hex2bin } from 'uint8-util'
|
||||
import { fontRx } from './util.js'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
|
||||
const debug = Debug('torrent:parser')
|
||||
|
||||
export default class Parser {
|
||||
parsed = false
|
||||
/** @type {Metadata} */
|
||||
|
|
@ -10,13 +13,16 @@ export default class Parser {
|
|||
client = null
|
||||
file = null
|
||||
destroyed = false
|
||||
|
||||
constructor (client, file) {
|
||||
debug('Initializing parser for file: ' + file.name)
|
||||
this.client = client
|
||||
this.file = file
|
||||
this.metadata = new Metadata(file)
|
||||
|
||||
this.metadata.getTracks().then(tracks => {
|
||||
if (this.destroyed) return
|
||||
debug('Tracks received: ' + tracks)
|
||||
if (!tracks.length) {
|
||||
this.parsed = true
|
||||
this.destroy()
|
||||
|
|
@ -27,17 +33,20 @@ export default class Parser {
|
|||
|
||||
this.metadata.getChapters().then(chapters => {
|
||||
if (this.destroyed) return
|
||||
debug(`Found ${chapters.length} chapters`)
|
||||
this.client.dispatch('chapters', chapters)
|
||||
})
|
||||
|
||||
this.metadata.getAttachments().then(files => {
|
||||
if (this.destroyed) return
|
||||
debug(`Found ${files.length} attachments`)
|
||||
for (const file of files) {
|
||||
if (fontRx.test(file.filename) || file.mimetype?.toLowerCase().includes('font')) {
|
||||
// this is cursed, but required, as capacitor-node's IPC hangs for 2mins when runnig on 32bit android when sending uint8's
|
||||
const data = hex2bin(arr2hex(file.data))
|
||||
// IPC crashes if the message is >16MB, wild
|
||||
if (SUPPORTS.isAndroid && data.length > 15_000_000) continue
|
||||
if (SUPPORTS.isAndroid && data.length > 15_000_000) {
|
||||
debug('Skipping large font file on Android: ' + file.filename)
|
||||
continue
|
||||
}
|
||||
this.client.dispatch('file', data)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +54,7 @@ export default class Parser {
|
|||
|
||||
this.metadata.on('subtitle', (subtitle, trackNumber) => {
|
||||
if (this.destroyed) return
|
||||
debug(`Found subtitle for track: ${trackNumber}: ${subtitle.text}`)
|
||||
this.client.dispatch('subtitle', { subtitle, trackNumber })
|
||||
})
|
||||
|
||||
|
|
@ -53,20 +63,14 @@ export default class Parser {
|
|||
if (this.destroyed) return cb(iterator)
|
||||
cb(this.metadata.parseStream(iterator))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async parseSubtitles () {
|
||||
if (this.file.name.endsWith('.mkv') || this.file.name.endsWith('.webm')) {
|
||||
console.log('Sub parsing started')
|
||||
await this.metadata.parseFile()
|
||||
console.log('Sub parsing finished')
|
||||
} else {
|
||||
debug('Unsupported file format: ' + this.file.name)
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
debug('Destroying Parser')
|
||||
this.destroyed = true
|
||||
this.metadata?.destroy()
|
||||
this.metadata = undefined
|
||||
// Add any additional cleanup code here
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ import { getEpisodeMetadataForMedia } from './anime.js'
|
|||
import AnimeResolver from '@/modules/animeresolver.js'
|
||||
import { hasNextPage } from '@/modules/sections.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import Debug from 'debug'
|
||||
|
||||
export const exclusions = ['DTS', '[EMBER]']
|
||||
const debug = Debug('ui:rss')
|
||||
|
||||
export const exclusions = ['DTS', 'TrueHD', '[EMBER]']
|
||||
const isDev = location.hostname === 'localhost'
|
||||
|
||||
const video = document.createElement('video')
|
||||
|
|
@ -54,6 +57,7 @@ export async function getRSSContent (url) {
|
|||
if (!url) return null
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
debug(`Failed to fetch RSS feed: ${res.statusText}`)
|
||||
throw new Error(res.statusText)
|
||||
}
|
||||
return DOMPARSER(await res.text(), 'text/xml')
|
||||
|
|
@ -69,9 +73,9 @@ class RSSMediaManager {
|
|||
if (!ignoreErrors) {
|
||||
res.catch(e => {
|
||||
toast.error('Search Failed', {
|
||||
description: 'Failed to loading media for home feed!\n' + e.message
|
||||
description: 'Failed to load media for home feed!\n' + e.message
|
||||
})
|
||||
console.error('Failed to loading media for home feed', e)
|
||||
debug('Failed to load media for home feed', e.stack)
|
||||
})
|
||||
}
|
||||
return Array.from({ length: perPage }, (_, i) => ({ type: 'episode', data: this.fromPending(res, i) }))
|
||||
|
|
@ -93,8 +97,10 @@ class RSSMediaManager {
|
|||
}
|
||||
|
||||
async _getMediaForRSS (page, perPage, url) {
|
||||
debug(`Getting media for RSS feed ${url} page ${page} perPage ${perPage}`)
|
||||
const changed = await this.getContentChanged(page, perPage, url)
|
||||
if (!changed) return this.resultMap[url].result
|
||||
debug(`Feed ${url} has changed, updating`)
|
||||
|
||||
const index = (page - 1) * perPage
|
||||
const targetPage = [...changed.content.querySelectorAll('item')].slice(index, index + perPage)
|
||||
|
|
@ -116,6 +122,7 @@ class RSSMediaManager {
|
|||
|
||||
const res = await Promise.all(await results)
|
||||
const newReleases = res.filter(({ date }) => date > oldDate)
|
||||
debug(`Found ${newReleases.length} new releases, notifying...`)
|
||||
|
||||
for (const { media, parseObject, episode } of newReleases) {
|
||||
const options = {
|
||||
|
|
@ -141,7 +148,7 @@ class RSSMediaManager {
|
|||
try {
|
||||
res.episodeData = (await getEpisodeMetadataForMedia(res.media))?.[res.episode]
|
||||
} catch (e) {
|
||||
console.warn('failed fetching episode metadata', e)
|
||||
debug(`Warn: failed fetching episode metadata for ${res.media.title.userPreferred} episode ${res.episode}: ${e.stack}`)
|
||||
}
|
||||
}
|
||||
res.date = items[i].date
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { writable } from 'simple-store-svelte'
|
|||
import { defaults } from './util.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:anilist')
|
||||
|
||||
/** @type {{viewer: import('./al').Query<{Viewer: import('./al').Viewer}>, token: string} | null} */
|
||||
export let alToken = JSON.parse(localStorage.getItem('ALviewer')) || null
|
||||
/** @type {{viewer: import('./mal').Query<{Viewer: import('./mal').Viewer}>, token: string} | null} */
|
||||
|
|
@ -70,7 +74,7 @@ async function handleToken (token) {
|
|||
const viewer = await anilistClient.viewer({token})
|
||||
if (!viewer.data?.Viewer) {
|
||||
toast.error('Failed to sign in with AniList. Please try again.', {description: JSON.stringify(viewer)})
|
||||
console.error(viewer)
|
||||
debug(`Failed to sign in with AniList: ${JSON.stringify(viewer)}`)
|
||||
return
|
||||
}
|
||||
const lists = viewer?.data?.Viewer?.mediaListOptions?.animeList?.customLists || []
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { toast } from 'svelte-sonner'
|
|||
import clipboard from './clipboard.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import 'browser-event-target-emitter'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:torrent')
|
||||
|
||||
const torrentRx = /(^magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent$){1}/i
|
||||
|
||||
|
|
@ -42,7 +45,7 @@ class TorrentWorker extends EventTarget {
|
|||
|
||||
async send (type, data, transfer) {
|
||||
await this.ready
|
||||
console.info('Torrent: sending message', { type, data })
|
||||
debug(`Sending message ${type}`, data)
|
||||
this.port.postMessage({ type, data }, transfer)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,18 +59,18 @@ client.on('files', ({ detail }) => {
|
|||
})
|
||||
|
||||
client.on('error', ({ detail }) => {
|
||||
console.error(detail)
|
||||
debug(`Error: ${detail.message || detail}`)
|
||||
toast.error('Torrent Error', { description: '' + (detail.message || detail) })
|
||||
})
|
||||
|
||||
client.on('warn', ({ detail }) => {
|
||||
console.error(detail)
|
||||
debug(`Warn: ${detail.message || detail}`)
|
||||
toast.warning('Torrent Warning', { description: '' + (detail.message || detail) })
|
||||
})
|
||||
|
||||
export async function add (torrentID, hide) {
|
||||
if (torrentID) {
|
||||
console.info('Torrent: adding torrent', { torrentID })
|
||||
debug('Adding torrent', { torrentID })
|
||||
if (torrentID.startsWith?.('magnet:')) {
|
||||
localStorage.setItem('torrent', JSON.stringify(torrentID))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { spawn } from 'node:child_process'
|
||||
import Debug from 'debug'
|
||||
import WebTorrent from 'webtorrent'
|
||||
import querystring from 'querystring'
|
||||
import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js'
|
||||
|
|
@ -17,6 +18,8 @@ const querystringStringify = obj => {
|
|||
return ret
|
||||
}
|
||||
|
||||
const debug = Debug('torrent:worker')
|
||||
|
||||
const ANNOUNCE = [
|
||||
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
|
||||
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
|
||||
|
|
@ -52,6 +55,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
|
||||
constructor (ipc, storageQuota, serverMode, torrentPath, controller) {
|
||||
const settings = { ...defaults, ...storedSettings }
|
||||
debug('Initializing TorrentClient with settings: ' + JSON.stringify(settings))
|
||||
super({
|
||||
dht: !settings.torrentDHT,
|
||||
maxConns: settings.maxConns,
|
||||
|
|
@ -67,6 +71,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
ipc.on('port', ({ ports }) => {
|
||||
this.message = ports[0].postMessage.bind(ports[0])
|
||||
ports[0].onmessage = ({ data }) => {
|
||||
debug(`Received IPC message ${data.type}: ${data.data}`)
|
||||
if (data.type === 'load') this.loadLastTorrent(data.data)
|
||||
if (data.type === 'destroy') this.destroy()
|
||||
this.handleMessage({ data })
|
||||
|
|
@ -119,6 +124,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
}
|
||||
|
||||
loadLastTorrent (t) {
|
||||
debug('Loading last torrent: ' + t)
|
||||
if (!t) return
|
||||
let torrent
|
||||
// this can be a magnet string, or a stringified array, lazy way of makign sure it works
|
||||
|
|
@ -136,6 +142,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
}
|
||||
|
||||
async handleTorrent (torrent) {
|
||||
debug('Got torrent metadata: ' + torrent.name)
|
||||
const files = torrent.files.map(file => {
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
|
|
@ -165,6 +172,8 @@ export default class TorrentClient extends WebTorrent {
|
|||
map[file.name] = file
|
||||
}
|
||||
|
||||
debug(`Found ${Object.keys(map).length} font files`)
|
||||
|
||||
for (const file of Object.values(map)) {
|
||||
const data = await file.arrayBuffer()
|
||||
if (targetFile !== this.current) return
|
||||
|
|
@ -180,6 +189,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
const subfiles = files.filter(file => {
|
||||
return subRx.test(file.name) && (videoFiles.length === 1 ? true : file.name.includes(videoName))
|
||||
})
|
||||
debug(`Found ${subfiles.length} subtitle files`)
|
||||
for (const file of subfiles) {
|
||||
const data = await file.arrayBuffer()
|
||||
if (targetFile !== this.current) return
|
||||
|
|
@ -188,6 +198,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
}
|
||||
|
||||
async _scrape ({ id, infoHashes }) {
|
||||
debug(`Scraping ${infoHashes.length} hashes, id: ${id}`)
|
||||
// this seems to give the best speed, and lowest failure rate
|
||||
const MAX_ANNOUNCE_LENGTH = 1300 // it's likely 2048, but lets undercut it
|
||||
const RATE_LIMIT = 200 // ms
|
||||
|
|
@ -205,11 +216,13 @@ export default class TorrentClient extends WebTorrent {
|
|||
this.tracker._request(this.tracker.scrapeUrl, { info_hash: batch }, (err, data) => {
|
||||
if (err) {
|
||||
const error = this._errorToString(err)
|
||||
debug('Failed to scrape: ' + error)
|
||||
this.dispatch('warn', `Failed to update seeder counts: ${error}`)
|
||||
return resolve([])
|
||||
}
|
||||
const { files } = data
|
||||
const result = []
|
||||
debug(`Scraped ${Object.keys(files || {}).length} hashes, id: ${id}`)
|
||||
for (const [key, data] of Object.entries(files || {})) {
|
||||
result.push({ hash: key.length !== 40 ? arr2hex(text2arr(key)) : key, ...data })
|
||||
}
|
||||
|
|
@ -233,6 +246,8 @@ export default class TorrentClient extends WebTorrent {
|
|||
}
|
||||
if (batch.length) await scrape()
|
||||
|
||||
debug(`Scraped ${results.length} hashes, id: ${id}`)
|
||||
|
||||
this.dispatch('scrape', { id, result: results })
|
||||
}
|
||||
|
||||
|
|
@ -259,11 +274,12 @@ export default class TorrentClient extends WebTorrent {
|
|||
for (const exclude of TorrentClient.excludedErrorMessages) {
|
||||
if (error.startsWith(exclude)) return
|
||||
}
|
||||
console.error(error)
|
||||
debug('Error: ' + error)
|
||||
this.dispatch('error', error)
|
||||
}
|
||||
|
||||
async addTorrent (data, skipVerify = false) {
|
||||
debug('Adding torrent: ' + data)
|
||||
const existing = await this.get(data)
|
||||
if (existing) {
|
||||
if (existing.ready) this.handleTorrent(existing)
|
||||
|
|
@ -333,6 +349,10 @@ export default class TorrentClient extends WebTorrent {
|
|||
this.addTorrent(data.data)
|
||||
break
|
||||
}
|
||||
case 'debug': {
|
||||
Debug.disable()
|
||||
if (data.data) Debug.enable(data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +362,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
}
|
||||
|
||||
destroy () {
|
||||
debug('Destroying TorrentClient')
|
||||
if (this.destroyed) return
|
||||
this.parser?.destroy()
|
||||
this.server.close()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"bottleneck": "^2.19.5",
|
||||
"browser-event-target-emitter": "^1.0.1",
|
||||
"comlink": "^4.4.1",
|
||||
"jassub": "latest",
|
||||
"jassub": "^1.7.17",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"p2pt": "github:ThaUnknown/p2pt#modernise",
|
||||
"perfect-seekbar": "^1.1.0",
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"svelte-keybinds": "^1.0.6",
|
||||
"svelte-loader": "^3.1.9",
|
||||
"svelte-miniplayer": "^1.0.5",
|
||||
"svelte-persisted-store": "^0.11.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"video-deband": "^1.0.5",
|
||||
"webpack-merge": "^5.10.0"
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { settings } from '@/modules/settings.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
let block = false
|
||||
|
||||
async function testConnection () {
|
||||
try {
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
// fetch twice, sometimes it will go tru once if ISP is shitty
|
||||
await fetch($settings.toshoURL + 'json?show=torrent&id=1')
|
||||
}
|
||||
block = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
block = true
|
||||
}
|
||||
}
|
||||
testConnection()
|
||||
</script>
|
||||
|
||||
{#if block}
|
||||
<div class='w-full h-full left-0 z-50 px-10 position-absolute content-wrapper bg-dark d-flex align-items-center justify-content-center flex-column'>
|
||||
<div>
|
||||
<h1 class='font-weight-bold'>Could not connect to Torrent API!</h1>
|
||||
<div class='font-size-16'>This happens either because the API is down, or because your ISP blocks the API, the latter being more likely.</div>
|
||||
<div class='font-size-16'>Most features of Miru will not function correctly without being able to connect to an API.</div>
|
||||
<div class='font-size-16'>If you enable a VPN a restart might be required for it to take effect.</div>
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<div class='font-size-16'>Visit <a class='text-primary pointer' use:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>this guide</a> for a tutorial on how to bypass ISP blocks.</div>
|
||||
<div class='d-flex w-full mt-20 pt-20'>
|
||||
<button class='btn ml-auto mr-5' type='button' use:click={() => { block = false }}>I Understand</button>
|
||||
<button class='btn btn-primary mr-5' type='button' use:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>Open Guide</button>
|
||||
<button class='btn btn-primary' type='button' use:click={testConnection}>Reconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
import { state } from '../WatchTogether/WatchTogether.svelte'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { anilistClient } from "@/modules/anilist.js"
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:mediahandler')
|
||||
|
||||
const episodeRx = /Episode (\d+) - (.*)/
|
||||
|
||||
|
|
@ -109,8 +112,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
function fileListToDebug (files) {
|
||||
return files.map(({ name, media, url }) => `\n${name} ${media?.parseObject.anime_title} ${media?.parseObject.episode_number} ${media?.media?.title.userPreferred} ${media?.episode}`).join('')
|
||||
}
|
||||
|
||||
async function handleFiles (files) {
|
||||
console.info('MediaHandler: got files', files)
|
||||
debug(`Got ${files.length} files`, fileListToDebug(files))
|
||||
if (!files?.length) return processed.set(files)
|
||||
let videoFiles = []
|
||||
const otherFiles = []
|
||||
|
|
@ -133,34 +140,35 @@
|
|||
videoFiles = videoFiles.filter(file => !TYPE_EXCLUSIONS.includes(file.media.parseObject.anime_type?.toUpperCase()))
|
||||
|
||||
if (nowPlaying?.verified && videoFiles.length === 1) {
|
||||
console.info('Media was verified, skipping verification')
|
||||
debug('Media was verified, skipping verification')
|
||||
videoFiles[0].media.media = nowPlaying.media
|
||||
if (nowPlaying.episode) videoFiles[0].media.episode = nowPlaying.episode
|
||||
}
|
||||
|
||||
console.info('MediaHandler: resolved video files', { videoFiles })
|
||||
debug(`Resolved ${videoFiles.length} video files`, fileListToDebug(videoFiles))
|
||||
|
||||
if (!nowPlaying) {
|
||||
nowPlaying = findPreferredPlaybackMedia(videoFiles)
|
||||
debug(`Found preferred playback media: ${nowPlaying.media?.id}:${nowPlaying.media?.title.userPreferred} ${nowPlaying.episode}`)
|
||||
}
|
||||
|
||||
const filtered = nowPlaying?.media && videoFiles.filter(file => file.media?.media?.id && file.media?.media?.id === nowPlaying.media.id)
|
||||
|
||||
console.info('MediaHandler: filtered files based on media', filtered)
|
||||
debug(`Filtered ${filtered?.length} files based on media`, fileListToDebug(filtered))
|
||||
|
||||
let result
|
||||
if (filtered?.length) {
|
||||
result = filtered
|
||||
} else {
|
||||
const max = highestOccurence(videoFiles, file => file.media.parseObject.anime_title).media.parseObject.anime_title
|
||||
console.info('MediaHandler: filtering based on highest occurence', max)
|
||||
debug(`Highest occurence anime title: ${max}`)
|
||||
result = videoFiles.filter(file => file.media.parseObject.anime_title === max)
|
||||
}
|
||||
|
||||
result.sort((a, b) => a.media.episode - b.media.episode)
|
||||
result.sort((a, b) => (b.media.parseObject.anime_season ?? 1) - (a.media.parseObject.anime_season ?? 1))
|
||||
|
||||
console.info('MediaHandler: final resolve result', { result })
|
||||
debug(`Sorted ${result.length} files`, fileListToDebug(result))
|
||||
|
||||
processed.set([...result, ...otherFiles])
|
||||
await tick()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,19 @@
|
|||
import { resetSettings } from '@/modules/settings.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import SettingCard from './SettingCard.svelte'
|
||||
|
||||
async function importSettings () {
|
||||
localStorage.setItem('settings', await navigator.clipboard.readText())
|
||||
location.reload()
|
||||
try {
|
||||
const settings = JSON.parse(await navigator.clipboard.readText())
|
||||
localStorage.setItem('settings', JSON.stringify(settings))
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
toast.error('Failed to import settings', {
|
||||
description: 'Failed to import settings from clipboard, make sure the copied data is valid JSON.',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
function exportSettings () {
|
||||
navigator.clipboard.writeText(localStorage.getItem('settings'))
|
||||
|
|
@ -24,31 +33,107 @@
|
|||
IPC.emit('update')
|
||||
}
|
||||
setInterval(checkUpdate, 1200000)
|
||||
|
||||
IPC.on('log-contents', log => {
|
||||
navigator.clipboard.writeText(log)
|
||||
toast.success('Copied to clipboard', {
|
||||
description: 'Copied log contents to clipboard',
|
||||
duration: 5000
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Debug from 'debug'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
const debug = persisted('debug', '', {
|
||||
serializer: {
|
||||
parse: e => e,
|
||||
stringify: e => e
|
||||
}
|
||||
})
|
||||
|
||||
export let version = ''
|
||||
export let settings
|
||||
|
||||
function updateDebug (debug) {
|
||||
Debug.disable()
|
||||
if (debug) Debug.enable(debug)
|
||||
client.send('debug', debug)
|
||||
}
|
||||
|
||||
$: updateDebug($debug)
|
||||
|
||||
onDestroy(() => {
|
||||
IPC.off('device-info', writeAppInfo)
|
||||
})
|
||||
|
||||
function writeAppInfo (info) {
|
||||
const deviceInfo = JSON.parse(info)
|
||||
deviceInfo.appInfo = {
|
||||
version,
|
||||
platform: window.version.platform,
|
||||
userAgent: navigator.userAgent,
|
||||
support: SUPPORTS,
|
||||
settings
|
||||
}
|
||||
navigator.clipboard.writeText(JSON.stringify(deviceInfo, null, 2))
|
||||
toast.success('Copied to clipboard', {
|
||||
description: 'Copied device info to clipboard',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
|
||||
IPC.on('device-info', writeAppInfo)
|
||||
</script>
|
||||
|
||||
<h4 class='mb-10 font-weight-bold'>Debug Settings</h4>
|
||||
<SettingCard title='Logging Levels' description='Enable logging of specific parts of the app. These logs are saved to %appdata$/Miru/logs/main.log or ~/config/Miru/logs/main.log.'>
|
||||
<select class='form-control bg-dark w-300 mw-full' bind:value={$debug}>
|
||||
<option value='' selected>None</option>
|
||||
<option value='*'>All</option>
|
||||
<option value='torrent:*,webtorrent:*,simple-peer,bittorrent-protocol,bittorrent-dht,bittorrent-lsd,torrent-discovery,bittorrent-tracker:*,ut_metadata,nat-pmp,nat-api'>Torrent</option>
|
||||
<option value='ui:*'>Interface</option>
|
||||
</select>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard title='App and Device Info' description='Copy app and device debug info and capabilities, such as GPU information, GPU capabilities, version information and settings to clipboard.'>
|
||||
<button type='button' use:click={() => IPC.emit('get-device-info')} class='btn btn-primary'>Copy To Clipboard</button>
|
||||
</SettingCard>
|
||||
|
||||
{#if !SUPPORTS.isAndroid}
|
||||
<SettingCard title='Log Output' description='Copy debug logs to clipboard. Once you enable a logging level you can use this to quickly copy the created logs to clipboard instead of navigating to the log file in directories.'>
|
||||
<button type='button' use:click={() => IPC.emit('get-log-contents')} class='btn btn-primary'>Copy To Clipboard</button>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard title='Open Torrent Devtools' description="Open devtools for the detached torrent process, this allows to inspect code execution and memory. DO NOT PASTE ANY CODE IN THERE, YOU'RE LIKELY BEING SCAMMED IF SOMEONE TELLS YOU TO!">
|
||||
<button type='button' use:click={() => IPC.emit('torrent-devtools')} class='btn btn-primary'>Open Devtools</button>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard title='Open UI Devtools' description="Open devtools for the UI process, this allows to inspect media playback information, rendering performance and more. DO NOT PASTE ANY CODE IN THERE, YOU'RE LIKELY BEING SCAMMED IF SOMEONE TELLS YOU TO!">
|
||||
<button type='button' use:click={() => IPC.emit('ui-devtools')} class='btn btn-primary'>Open Devtools</button>
|
||||
</SettingCard>
|
||||
{/if}
|
||||
|
||||
<h4 class='mb-10 font-weight-bold'>App Settings</h4>
|
||||
<div class='d-inline-flex flex-column'>
|
||||
<button
|
||||
use:click={importSettings}
|
||||
class='btn btn-primary mx-20 mt-10'
|
||||
type='button'>
|
||||
<button use:click={importSettings} class='btn btn-primary mt-10' type='button'>
|
||||
Import Settings From Clipboard
|
||||
</button>
|
||||
<button
|
||||
use:click={exportSettings}
|
||||
class='btn btn-primary mx-20 mt-10'
|
||||
type='button'>
|
||||
<button use:click={exportSettings} class='btn btn-primary mt-10' type='button'>
|
||||
Export Settings To Clipboard
|
||||
</button>
|
||||
{#if SUPPORTS.update}
|
||||
<button
|
||||
use:click={checkUpdate}
|
||||
class='btn btn-primary mx-20 mt-10'
|
||||
type='button'>
|
||||
<button use:click={checkUpdate} class='btn btn-primary mt-10' type='button'>
|
||||
Check For Updates
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
use:click={restoreSettigs}
|
||||
class='btn btn-danger mx-20 mt-10'
|
||||
class='btn btn-danger mt-10'
|
||||
type='button'
|
||||
data-toggle='tooltip'
|
||||
data-placement='top'
|
||||
|
|
|
|||
|
|
@ -75,18 +75,18 @@
|
|||
<button type='button' use:click={() => { homeSections[homeSections.length] = 'Trending Now' }} class='btn btn-primary'>Add Section</button>
|
||||
|
||||
<style>
|
||||
.ghost {
|
||||
margin-bottom: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
position: absolute !important;
|
||||
}
|
||||
.ghost {
|
||||
margin-bottom: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.tp {
|
||||
opacity: 0;
|
||||
}
|
||||
.tp {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.grab{
|
||||
cursor: grab;
|
||||
}
|
||||
.grab{
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@
|
|||
</Tab>
|
||||
<Tab>
|
||||
<div class='root h-full w-full overflow-y-md-auto p-20' use:smoothScroll>
|
||||
<AppSettings />
|
||||
<AppSettings {version} settings={$settings} />
|
||||
<div class='h-250' />
|
||||
</div>
|
||||
</Tab>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import getResultsFromExtensions from '@/modules/extensions/index.js'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:extensions')
|
||||
|
||||
/** @typedef {import('@/modules/al.d.ts').Media} Media */
|
||||
|
||||
|
|
@ -84,7 +87,7 @@
|
|||
$: autoPlay(best, $settings.rssAutoplay)
|
||||
|
||||
$: lookup.catch(err => {
|
||||
console.error(err)
|
||||
debug(`Error fetching torrents for ${search.media.title.userPreferred} Episode ${search.episode}, ${err.stack}`)
|
||||
toast.error(`No torrent found for ${anilistClient.title(search.media)} Episode ${search.episode}!`, { description: err.message })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -164,11 +164,9 @@
|
|||
{playButtonText}
|
||||
</button>
|
||||
<div class='mt-20'>
|
||||
{#if Helper.isAniAuth()}
|
||||
<button class='btn bg-dark btn-lg btn-square material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite}>
|
||||
favorite
|
||||
</button>
|
||||
{/if}
|
||||
<button class='btn bg-dark btn-lg btn-square material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
</button>
|
||||
<Scoring {media} viewAnime={true} />
|
||||
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
|
||||
share
|
||||
|
|
@ -246,7 +244,7 @@
|
|||
<div class='ml-auto pl-20 font-size-12 more text-muted text-nowrap' use:click={() => { episodeOrder = !episodeOrder }}>Reverse</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-lg-5 col-12 d-flex flex-column pl-lg-20'>
|
||||
<div class='col-lg-5 col-12 d-flex flex-column pl-lg-20 overflow-x-hidden'>
|
||||
<EpisodeList {media} {episodeOrder} userProgress={['CURRENT', 'PAUSED', 'DROPPED'].includes(media.mediaListEntry?.status) && media.mediaListEntry.progress} watched={media.mediaListEntry?.status === 'COMPLETED'} episodeCount={getMediaMaxEp(media)} {play} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -291,7 +289,7 @@
|
|||
width: 23rem !important;
|
||||
}
|
||||
|
||||
button.bg-dark:hover {
|
||||
button.bg-dark:not([disabled]):hover {
|
||||
background: #292d33 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
import Helper from '@/modules/helper.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import 'browser-event-target-emitter'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:w2g')
|
||||
|
||||
export const w2gEmitter = new EventTarget()
|
||||
|
||||
|
|
@ -19,6 +22,7 @@
|
|||
let p2pt = null
|
||||
|
||||
function joinLobby (code = generateRandomHexCode(16)) {
|
||||
debug('Joining lobby with code: ' + code)
|
||||
if (p2pt) cleanup()
|
||||
p2pt = new P2PT([
|
||||
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
|
||||
|
|
@ -27,8 +31,7 @@
|
|||
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
|
||||
], code)
|
||||
p2pt.on('peerconnect', peer => {
|
||||
console.log(peer.id)
|
||||
console.log('connect')
|
||||
debug(`Peer connected: ${peer.id}`)
|
||||
const user = Helper.getUser() || {}
|
||||
p2pt.send(peer,
|
||||
JSON.stringify({
|
||||
|
|
@ -40,18 +43,16 @@
|
|||
})
|
||||
p2pt.on('peerclose', peer => {
|
||||
peers.update(object => {
|
||||
console.log(peer.id)
|
||||
console.log('close', object[peer.id])
|
||||
debug(`Peer disconnected: ${peer.id}`)
|
||||
delete object[peer.id]
|
||||
return object
|
||||
})
|
||||
})
|
||||
p2pt.on('msg', (peer, data) => {
|
||||
console.log(data)
|
||||
debug(`Received message from ${peer.id}: ${data.type} ${data}`)
|
||||
data = typeof data === 'string' ? JSON.parse(data) : data
|
||||
switch (data.type) {
|
||||
case 'init':
|
||||
console.log('init', data.user)
|
||||
peers.update(object => {
|
||||
object[peer.id] = {
|
||||
peer,
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
break
|
||||
}
|
||||
default: {
|
||||
console.error('Invalid message type', data)
|
||||
debug('Invalid message type: ' + data.type)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -117,6 +118,7 @@
|
|||
})
|
||||
|
||||
function emit (type, data) {
|
||||
debug(`Emitting ${type} with data: ${JSON.stringify(data)}`)
|
||||
if (p2pt) {
|
||||
for (const { peer } of Object.values(peers.value)) {
|
||||
p2pt.send(peer, { type, ...data })
|
||||
|
|
@ -164,7 +166,6 @@
|
|||
if (!invite) return
|
||||
const match = invite?.match(inviteRx)?.[1]
|
||||
if (!match) return
|
||||
console.log(match)
|
||||
page.set('watchtogether')
|
||||
joinLobby(match)
|
||||
joinText = ''
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ module.exports = (parentDir, alias = {}, aliasFields = 'browser', filename = 'ap
|
|||
'@': __dirname,
|
||||
module: false,
|
||||
url: false,
|
||||
debug: resolve(__dirname, './modules/debug.js'),
|
||||
'bittorrent-tracker/lib/client/websocket-tracker.js': resolve('../node_modules/bittorrent-tracker/lib/client/websocket-tracker.js')
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.svelte']
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "5.2.16",
|
||||
"version": "5.3.4",
|
||||
"private": true,
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"description": "Stream anime torrents, real-time with no waiting for downloads.",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Protocol from './protocol.js'
|
|||
import Updater from './updater.js'
|
||||
import Dialog from './dialog.js'
|
||||
import store from './store.js'
|
||||
import Debug from './debugger.js'
|
||||
|
||||
export default class App {
|
||||
webtorrentWindow = new BrowserWindow({
|
||||
|
|
@ -50,6 +51,7 @@ export default class App {
|
|||
protocol = new Protocol(this.mainWindow)
|
||||
updater = new Updater(this.mainWindow, this.webtorrentWindow)
|
||||
dialog = new Dialog(this.webtorrentWindow)
|
||||
debug = new Debug()
|
||||
|
||||
constructor () {
|
||||
this.mainWindow.setMenuBarVisibility(false)
|
||||
|
|
@ -57,7 +59,8 @@ export default class App {
|
|||
this.mainWindow.once('ready-to-show', () => this.mainWindow.show())
|
||||
this.mainWindow.on('minimize', () => this.mainWindow.webContents.postMessage('visibilitychange', 'hidden'))
|
||||
this.mainWindow.on('restore', () => this.mainWindow.webContents.postMessage('visibilitychange', 'visible'))
|
||||
ipcMain.on('devtools', () => this.webtorrentWindow.webContents.openDevTools())
|
||||
ipcMain.on('torrent-devtools', () => this.webtorrentWindow.webContents.openDevTools())
|
||||
ipcMain.on('ui-devtools', ({ sender }) => sender.openDevTools())
|
||||
|
||||
this.mainWindow.on('closed', () => this.destroy())
|
||||
ipcMain.on('close', () => this.destroy())
|
||||
|
|
|
|||
29
electron/src/main/debugger.js
Normal file
29
electron/src/main/debugger.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
|
||||
log.initialize({ spyRendererConsole: true })
|
||||
log.transports.file.level = 'debug'
|
||||
log.transports.file.maxSize = 10485760 // 10MB
|
||||
autoUpdater.logger = log
|
||||
|
||||
export default class Debug {
|
||||
constructor () {
|
||||
ipcMain.on('get-log-contents', async ({ sender }) => {
|
||||
sender.send('log-contents', await readFile(log.transports.file.getFile().path, 'utf8'))
|
||||
})
|
||||
|
||||
ipcMain.on('get-device-info', async ({ sender }) => {
|
||||
const { model, speed } = os.cpus()[0]
|
||||
const deviceInfo = {
|
||||
features: app.getGPUFeatureStatus(),
|
||||
info: await app.getGPUInfo('complete'),
|
||||
cpu: { model, speed },
|
||||
ram: os.totalmem()
|
||||
}
|
||||
sender.send('device-info', JSON.stringify(deviceInfo))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
import log from 'electron-log'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { ipcMain, shell } from 'electron'
|
||||
|
||||
log.initialize({ spyRendererConsole: true })
|
||||
log.transports.file.level = 'info'
|
||||
autoUpdater.logger = log
|
||||
let notified = false
|
||||
ipcMain.on('update', () => {
|
||||
if (!notified) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ module.exports = [
|
|||
'node-fetch': false,
|
||||
ws: false,
|
||||
wrtc: false,
|
||||
debug: resolve(__dirname, '../common/modules/debug.js'),
|
||||
'bittorrent-tracker/lib/client/http-tracker.js': resolve('../node_modules/bittorrent-tracker/lib/client/http-tracker.js'),
|
||||
'webrtc-polyfill': resolve('../node_modules/webrtc-polyfill/browser.js')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ importers:
|
|||
'@capacitor/core':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
'@capacitor/device':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(@capacitor/core@6.1.1)
|
||||
'@capacitor/ios':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1(@capacitor/core@6.1.1)
|
||||
|
|
@ -158,8 +161,8 @@ importers:
|
|||
specifier: ^4.4.1
|
||||
version: 4.4.1
|
||||
jassub:
|
||||
specifier: latest
|
||||
version: 1.7.15
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17
|
||||
js-levenshtein:
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6
|
||||
|
|
@ -190,6 +193,9 @@ importers:
|
|||
svelte-miniplayer:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
svelte-persisted-store:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(svelte@4.2.12)
|
||||
svelte-sonner:
|
||||
specifier: ^0.3.19
|
||||
version: 0.3.19(svelte@4.2.12)
|
||||
|
|
@ -403,6 +409,14 @@ packages:
|
|||
tslib: 2.6.3
|
||||
dev: false
|
||||
|
||||
/@capacitor/device@6.0.1(@capacitor/core@6.1.1):
|
||||
resolution: {integrity: sha512-Tlz67DAO5GKb5YAfupXiENZxDww6mHnG9iKI+8D5SVF82VLpEv5r9qwKtiounuQB2y2HWiHV8tlOk7DqnLVUqQ==}
|
||||
peerDependencies:
|
||||
'@capacitor/core': ^6.0.0
|
||||
dependencies:
|
||||
'@capacitor/core': 6.1.1
|
||||
dev: false
|
||||
|
||||
/@capacitor/ios@6.1.1(@capacitor/core@6.1.1):
|
||||
resolution: {integrity: sha512-he6+Fhj6x1dSnOzM98xaPvioOU8MNO+qpodCJwnHE3mIRonTpFutXJK8DP8fnNhjDPk2km9VafLSfOeiZXNv3Q==}
|
||||
peerDependencies:
|
||||
|
|
@ -1386,7 +1400,7 @@ packages:
|
|||
vite: ^5.0.0
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.1.5)
|
||||
debug: 4.3.4
|
||||
debug: 4.3.6
|
||||
svelte: 4.2.12
|
||||
vite: 5.1.5
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -6024,8 +6038,8 @@ packages:
|
|||
minimatch: 3.1.2
|
||||
dev: true
|
||||
|
||||
/jassub@1.7.15:
|
||||
resolution: {integrity: sha512-8yKAJc++Y1gNfATOPRo3APk0JUhshKl5l7bRkT6WkJ8XP4RvYfVPb6ieH6WDxsMq523exwGzNvjjPEEWT+Z1nQ==}
|
||||
/jassub@1.7.17:
|
||||
resolution: {integrity: sha512-573efPTIYLh9YaauuSX2mgPrrYedqdeu6KYNNjsJXbLbBsxPijcUHrMK4zpXCeBv955VTUlMX6l0Afa8AXf33A==}
|
||||
dependencies:
|
||||
rvfc-polyfill: 1.0.7
|
||||
dev: false
|
||||
|
|
@ -8712,6 +8726,15 @@ packages:
|
|||
resolution: {integrity: sha512-jzYqqBuXcSH5KzoPDlYQL6CQVbpY2LQB4/wBPG4T5R75wE8Dqu4auMU6NnJxHBRhgNCGlH+XBQvxy9G6yX/XQw==}
|
||||
dev: false
|
||||
|
||||
/svelte-persisted-store@0.11.0(svelte@4.2.12):
|
||||
resolution: {integrity: sha512-9RgJ5DrawGyyfK22A80cfu8Jose3CV8YjEZKz9Tn94rQ0tWyEmYr+XI+wrVF6wjRbW99JMDSVcFRiM3XzVJj/w==}
|
||||
engines: {node: '>=0.14'}
|
||||
peerDependencies:
|
||||
svelte: ^3.48.0 || ^4.0.0 || ^5.0.0-next.0
|
||||
dependencies:
|
||||
svelte: 4.2.12
|
||||
dev: false
|
||||
|
||||
/svelte-preprocess@5.1.4(postcss@8.4.38)(svelte@4.2.12)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:window on:load={console.log} />
|
||||
|
||||
<style>
|
||||
@keyframes transition {
|
||||
0% {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { formatMap } from './anime.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import { alToken } from '@/modules/settings.js'
|
||||
export let media
|
||||
|
||||
let hide = true
|
||||
|
|
@ -82,10 +83,10 @@
|
|||
</span>
|
||||
{playButtonText}
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={noop}>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={noop} disabled={!alToken}>
|
||||
favorite
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={noop}>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={noop} disabled={!alToken}>
|
||||
bookmark
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue