feat: verbose debug logging

This commit is contained in:
ThaUnknown 2024-08-18 16:29:35 +02:00
parent eeed27ffcf
commit fdec580314
16 changed files with 179 additions and 90 deletions

View file

@ -11,7 +11,12 @@
page = 'home'
}
const debug = persisted('debug', '')
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'>

View file

@ -6,6 +6,9 @@ import { alToken } from '@/modules/settings.js'
import { toast } from 'svelte-sonner'
import { sleep } from './util.js'
import IPC from '@/modules/ipc.js'
import Debug from 'debug'
const debug = Debug('ui:anilist')
const codes = {
400: 'Bad Request',
@ -34,7 +37,7 @@ 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
@ -191,6 +194,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)
@ -282,10 +286,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,
@ -301,6 +307,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>} */
@ -361,7 +368,9 @@ class AnilistClient {
// check if values exist
if (filemedia.media && alToken) {
const { media, failed } = filemedia
debug(`Checking entry for ${media.title.userPreferred}`)
debug(`Media viability: ${media.status}, Is from failed resolve: ${failed}`)
if (failed) return
if (media.status !== 'FINISHED' && media.status !== 'RELEASING') return
@ -371,6 +380,7 @@ class AnilistClient {
const videoEpisode = Number(filemedia.episode) || singleEpisode
const mediaEpisode = media.episodes || media.nextAiringEpisode?.episode || singleEpisode
debug(`Episode viability: ${videoEpisode}, ${mediaEpisode}, ${singleEpisode}`)
if (!videoEpisode || !mediaEpisode) return
// check episode range, safety check if `failed` didn't catch this
if (videoEpisode > mediaEpisode) return
@ -380,10 +390,12 @@ class AnilistClient {
const status = media.mediaListEntry?.status === 'REPEATING' ? 'REPEATING' : 'CURRENT'
const progress = media.mediaListEntry?.progress
debug(`User's progress: ${progress}, Media's progress: ${videoEpisode}`)
// check user's own watch progress
if (progress > videoEpisode) return
if (progress === videoEpisode && videoEpisode !== mediaEpisode && !singleEpisode) return
debug(`Updating entry for ${media.title.userPreferred}`)
const variables = {
repeat: media.mediaListEntry?.repeat || 0,
id: media.id,
@ -404,6 +416,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) {
@ -427,6 +440,7 @@ class AnilistClient {
}
async searchIDSingle (variables) {
debug(`Searching for ID: ${variables.id}`)
const query = /* js */`
query($id: Int) {
Media(id: $id, type: ANIME) {
@ -443,6 +457,7 @@ class AnilistClient {
}
async searchIDS (variables) {
debug(`Searching for IDs: ${variables.id.length}`)
const query = /* js */`
query($id: [Int], $page: Int, $perPage: Int, $status: [MediaStatus], $onList: Boolean, $sort: [MediaSort], $search: String, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat) {
Page(page: $page, perPage: $perPage) {
@ -465,6 +480,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) {
@ -507,6 +523,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 {
@ -528,6 +545,7 @@ class AnilistClient {
/** @returns {Promise<import('./al.d.ts').Query<{ MediaListCollection: import('./al.d.ts').MediaListCollection }>>} */
async getUserLists (variables = {}) {
debug('Getting user lists')
const userId = this.userID?.viewer?.data?.Viewer.id
variables.id = userId
const query = /* js */`
@ -565,6 +583,7 @@ class AnilistClient {
/** @returns {Promise<import('./al.d.ts').Query<{ MediaList: { status: string, progress: number, repeat: number }}>>} */
async searchIDStatus (variables = {}) {
debug(`Searching for ID status: ${variables.id}`)
const userId = this.userID?.viewer?.data?.Viewer.id
variables.id = userId
const query = /* js */`
@ -580,6 +599,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) {
@ -607,7 +627,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) {
@ -622,6 +643,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, $format: MediaFormat) {
@ -645,6 +667,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) {
@ -657,6 +680,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 {
@ -678,6 +702,7 @@ class AnilistClient {
}
entry (variables) {
debug(`Updating entry for ${variables.id}`)
const query = /* js */`
mutation($lists: [String], $id: Int, $status: MediaListStatus, $episode: Int, $repeat: Int, $score: Int) {
SaveMediaListEntry(mediaId: $id, status: $status, progress: $episode, repeat: $repeat, scoreRaw: $score, customLists: $lists) {
@ -692,6 +717,7 @@ class AnilistClient {
}
delete (variables) {
debug(`Deleting entry for ${variables.id}`)
const query = /* js */`
mutation($id: Int) {
DeleteMediaListEntry(id: $id) {
@ -703,6 +729,7 @@ class AnilistClient {
}
favourite (variables) {
debug(`Toggling favourite for ${variables.id}`)
const query = /* js */`
mutation($id: Int) {
ToggleFavourite(animeId: $id) { anime { nodes { id } } }
@ -712,6 +739,7 @@ class AnilistClient {
}
customList (variables = {}) {
debug('Updating custom list')
variables.lists = [...variables.lists, 'Watched using Miru']
const query = /* js */`
mutation($lists: [String]) {

View file

@ -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,14 +140,18 @@ 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
const result = await this.resolveSeason({ media: root || media, episode: parseObj.episode_number[1], increment: !parseObj.anime_season ? null : true })
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])}`
@ -149,19 +161,24 @@ 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
const result = await this.resolveSeason({ media: root || media, episode: parseInt(parseObj.episode_number), increment: !parseObj.anime_season ? null : true })
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,
@ -172,6 +189,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) {
@ -186,9 +209,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
@ -205,7 +227,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)

View file

@ -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

View file

@ -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
}
}

View file

@ -6,6 +6,9 @@ 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'
const debug = Debug('ui:rss')
export const exclusions = ['DTS', 'TrueHD', '[EMBER]']
const isDev = location.hostname === 'localhost'
@ -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

View file

@ -3,6 +3,10 @@ import { defaults } from './util.js'
import IPC from '@/modules/ipc.js'
import { anilistClient } from './anilist.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
@ -54,7 +58,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 || []

View file

@ -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 {

View file

@ -18,6 +18,8 @@ const querystringStringify = obj => {
return ret
}
const debug = Debug('torrent:worker')
const ANNOUNCE = [
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
@ -53,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,
@ -68,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 })
@ -120,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
@ -137,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,
@ -166,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
@ -181,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
@ -189,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
@ -206,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 })
}
@ -234,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 })
}
@ -260,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)
@ -347,6 +362,7 @@ export default class TorrentClient extends WebTorrent {
}
destroy () {
debug('Destroying TorrentClient')
if (this.destroyed) return
this.parser?.destroy()
this.server.close()

View file

@ -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}

View file

@ -5,6 +5,9 @@
import { tick } from 'svelte'
import { state } from '../WatchTogether/WatchTogether.svelte'
import IPC from '@/modules/ipc.js'
import Debug from 'debug'
const debug = Debug('ui:mediahandler')
const episodeRx = /Episode (\d+) - (.*)/
@ -108,8 +111,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 = []
@ -132,34 +139,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()

View file

@ -49,7 +49,12 @@
import { client } from '@/modules/torrent.js'
import { onDestroy } from 'svelte'
const debug = persisted('debug', '')
const debug = persisted('debug', '', {
serializer: {
parse: e => e,
stringify: e => e
}
})
export let version = ''
export let settings
@ -90,7 +95,7 @@
<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:worker,webtorrent:*,simple-peer,bittorrent-protocol,bittorrent-dht,bittorrent-lsd,torrent-discovery,bittorrent-tracker:*,ut_metadata,nat-pmp,nat-api'>Torrent</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>

View file

@ -3,6 +3,9 @@
import { settings } from '@/modules/settings.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 */
@ -83,7 +86,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 ${search.media.title.userPreferred} Episode ${search.episode}!`, { description: err.message })
})

View file

@ -9,6 +9,9 @@
import { click } from '@/modules/click.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 = anilistClient.userID?.viewer?.data?.Viewer || {}
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 = ''

View file

@ -5,7 +5,7 @@ import log from 'electron-log'
import { autoUpdater } from 'electron-updater'
log.initialize({ spyRendererConsole: true })
log.transports.file.level = 'info'
log.transports.file.level = 'debug'
log.transports.file.maxSize = 10485760 // 10MB
autoUpdater.logger = log

View file

@ -14,8 +14,6 @@
</div>
</div>
<svelte:window on:load={console.log} />
<style>
@keyframes transition {
0% {