mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 02:21:49 +00:00
feat: debug logging
This commit is contained in:
parent
28283725e0
commit
7ca22198b3
15 changed files with 603 additions and 13 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "6.4.89",
|
"version": "6.4.90",
|
||||||
"license": "BUSL-1.1",
|
"license": "BUSL-1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.15.5",
|
"packageManager": "pnpm@9.15.5",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import { persisted } from 'svelte-persisted-store'
|
|
||||||
|
|
||||||
import Wrapper from './wrapper.svelte'
|
import Wrapper from './wrapper.svelte'
|
||||||
|
|
||||||
import native from '$lib/modules/native'
|
import native from '$lib/modules/native'
|
||||||
import { click } from '$lib/modules/navigate'
|
import { click } from '$lib/modules/navigate'
|
||||||
import { SUPPORTS } from '$lib/modules/settings'
|
import { debug, SUPPORTS } from '$lib/modules/settings'
|
||||||
|
|
||||||
const debug = persisted('debug', '')
|
|
||||||
function tabindex (node: HTMLElement) {
|
function tabindex (node: HTMLElement) {
|
||||||
node.tabIndex = -1
|
node.tabIndex = -1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { queryStore, type OperationResultState, gql as _gql } from '@urql/svelte'
|
import { queryStore, type OperationResultState, gql as _gql } from '@urql/svelte'
|
||||||
|
import Debug from 'debug'
|
||||||
import lavenshtein from 'js-levenshtein'
|
import lavenshtein from 'js-levenshtein'
|
||||||
import { derived, readable, writable, type Writable } from 'svelte/store'
|
import { derived, readable, writable, type Writable } from 'svelte/store'
|
||||||
|
|
||||||
|
|
@ -12,6 +13,8 @@ import type { AnyVariables, OperationContext, RequestPolicy, TypedDocumentNode }
|
||||||
|
|
||||||
import { arrayEqual } from '$lib/utils'
|
import { arrayEqual } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:anilist')
|
||||||
|
|
||||||
function getDistanceFromTitle (media: Media & {lavenshtein?: number}, name: string) {
|
function getDistanceFromTitle (media: Media & {lavenshtein?: number}, name: string) {
|
||||||
const titles = Object.values(media.title ?? {}).filter(v => v).map(title => lavenshtein(title?.toLowerCase() ?? '', name.toLowerCase()))
|
const titles = Object.values(media.title ?? {}).filter(v => v).map(title => lavenshtein(title?.toLowerCase() ?? '', name.toLowerCase()))
|
||||||
const synonyms = (media.synonyms ?? []).filter(v => v).map(title => lavenshtein(title?.toLowerCase() ?? '', name.toLowerCase()) + 2)
|
const synonyms = (media.synonyms ?? []).filter(v => v).map(title => lavenshtein(title?.toLowerCase() ?? '', name.toLowerCase()) + 2)
|
||||||
|
|
@ -25,18 +28,24 @@ class AnilistClient {
|
||||||
client: typeof urqlClient = urqlClient
|
client: typeof urqlClient = urqlClient
|
||||||
constructor () {
|
constructor () {
|
||||||
// hacky but prevents query from re-running
|
// hacky but prevents query from re-running
|
||||||
this.userlists.subscribe(() => undefined)
|
// the debug logging is added after an empty useless subscription, don't delete this subscription!
|
||||||
this.continueIDs.subscribe(() => undefined)
|
this.userlists.subscribe(ids => {
|
||||||
|
debug('userlists: ', ids?.data?.MediaListCollection)
|
||||||
|
})
|
||||||
|
this.continueIDs.subscribe(ids => {
|
||||||
|
debug('continueIDs: ', ids)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
userlists = derived<typeof this.client.viewer, OperationResultState<ResultOf<typeof UserLists>>>(this.client.viewer, (store, set) => {
|
userlists = derived<typeof this.client.viewer, OperationResultState<ResultOf<typeof UserLists>>>(this.client.viewer, (store, set) => {
|
||||||
return queryStore({ client: this.client, query: UserLists, variables: { id: store?.viewer?.id } }).subscribe(set)
|
return queryStore({ client: this.client, query: UserLists, variables: { id: store?.viewer?.id }, context: { requestPolicy: 'cache-and-network' } }).subscribe(set)
|
||||||
})
|
})
|
||||||
|
|
||||||
// WARN: these 3 sections are hacky, i use oldvalue to prevent re-running loops, I DO NOT KNOW WHY THE LOOPS HAPPEN!
|
// WARN: these 3 sections are hacky, i use oldvalue to prevent re-running loops, I DO NOT KNOW WHY THE LOOPS HAPPEN!
|
||||||
continueIDs = readable<number[]>([], set => {
|
continueIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
return this.userlists.subscribe(values => {
|
return this.userlists.subscribe(values => {
|
||||||
|
debug('continueIDs: checking for IDs')
|
||||||
if (!values.data?.MediaListCollection?.lists) return
|
if (!values.data?.MediaListCollection?.lists) return
|
||||||
const mediaList = values.data.MediaListCollection.lists.reduce<NonNullable<NonNullable<NonNullable<NonNullable<ResultOf<typeof UserLists>['MediaListCollection']>['lists']>[0]>['entries']>>((filtered, list) => {
|
const mediaList = values.data.MediaListCollection.lists.reduce<NonNullable<NonNullable<NonNullable<NonNullable<ResultOf<typeof UserLists>['MediaListCollection']>['lists']>[0]>['entries']>>((filtered, list) => {
|
||||||
return (list?.status === 'CURRENT' || list?.status === 'REPEATING') ? filtered.concat(list.entries) : filtered
|
return (list?.status === 'CURRENT' || list?.status === 'REPEATING') ? filtered.concat(list.entries) : filtered
|
||||||
|
|
@ -49,8 +58,10 @@ class AnilistClient {
|
||||||
return progress < (entry?.media?.nextAiringEpisode?.episode ?? (progress + 2)) - 1
|
return progress < (entry?.media?.nextAiringEpisode?.episode ?? (progress + 2)) - 1
|
||||||
}).map(entry => entry?.media?.id) as number[]
|
}).map(entry => entry?.media?.id) as number[]
|
||||||
|
|
||||||
|
debug('continueIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('continueIDs: updated IDs')
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -58,6 +69,7 @@ class AnilistClient {
|
||||||
sequelIDs = readable<number[]>([], set => {
|
sequelIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
return this.userlists.subscribe(values => {
|
return this.userlists.subscribe(values => {
|
||||||
|
debug('sequelIDs: checking for IDs')
|
||||||
if (!values.data?.MediaListCollection?.lists) return
|
if (!values.data?.MediaListCollection?.lists) return
|
||||||
const mediaList = values.data.MediaListCollection.lists.find(list => list?.status === 'COMPLETED')?.entries
|
const mediaList = values.data.MediaListCollection.lists.find(list => list?.status === 'COMPLETED')?.entries
|
||||||
if (!mediaList) return []
|
if (!mediaList) return []
|
||||||
|
|
@ -66,8 +78,10 @@ class AnilistClient {
|
||||||
return entry?.media?.relations?.edges?.filter(edge => edge?.relationType === 'SEQUEL')
|
return entry?.media?.relations?.edges?.filter(edge => edge?.relationType === 'SEQUEL')
|
||||||
}).map(edge => edge?.node?.id))] as number[]
|
}).map(edge => edge?.node?.id))] as number[]
|
||||||
|
|
||||||
|
debug('sequelIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('sequelIDs: updated IDs')
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -75,13 +89,16 @@ class AnilistClient {
|
||||||
planningIDs = readable<number[]>([], set => {
|
planningIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
return this.userlists.subscribe(userLists => {
|
return this.userlists.subscribe(userLists => {
|
||||||
|
debug('planningIDs: checking for IDs')
|
||||||
if (!userLists.data?.MediaListCollection?.lists) return
|
if (!userLists.data?.MediaListCollection?.lists) return
|
||||||
const mediaList = userLists.data.MediaListCollection.lists.find(list => list?.status === 'PLANNING')?.entries
|
const mediaList = userLists.data.MediaListCollection.lists.find(list => list?.status === 'PLANNING')?.entries
|
||||||
if (!mediaList) return []
|
if (!mediaList) return []
|
||||||
const ids = mediaList.map(entry => entry?.media?.id) as number[]
|
const ids = mediaList.map(entry => entry?.media?.id) as number[]
|
||||||
|
|
||||||
|
debug('planningIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('planningIDs: updated IDs')
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -92,6 +109,7 @@ class AnilistClient {
|
||||||
|
|
||||||
async searchCompound (flattenedTitles: Array<{key: string, title: string, year?: string, isAdult: boolean}>) {
|
async searchCompound (flattenedTitles: Array<{key: string, title: string, year?: string, isAdult: boolean}>) {
|
||||||
if (!flattenedTitles.length) return []
|
if (!flattenedTitles.length) return []
|
||||||
|
debug('searchCompound: searching for', flattenedTitles)
|
||||||
// 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
|
// 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
|
||||||
|
|
||||||
const requestVariables = flattenedTitles.reduce<Record<`v${number}`, string>>((obj, { title, isAdult }, i) => {
|
const requestVariables = flattenedTitles.reduce<Record<`v${number}`, string>>((obj, { title, isAdult }, i) => {
|
||||||
|
|
@ -129,6 +147,8 @@ class AnilistClient {
|
||||||
|
|
||||||
const res = await this.client.query<Record<string, {media: Media[]}>>(query, requestVariables)
|
const res = await this.client.query<Record<string, {media: Media[]}>>(query, requestVariables)
|
||||||
|
|
||||||
|
debug('searchCompound: received response', res)
|
||||||
|
|
||||||
if (!res.data) return []
|
if (!res.data) return []
|
||||||
|
|
||||||
const searchResults: Record<string, number> = {}
|
const searchResults: Record<string, number> = {}
|
||||||
|
|
@ -140,7 +160,9 @@ class AnilistClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = Object.values(searchResults)
|
const ids = Object.values(searchResults)
|
||||||
|
debug('searchCompound: found IDs', ids)
|
||||||
const search = await this.client.query(Search, { ids, perPage: 50 })
|
const search = await this.client.query(Search, { ids, perPage: 50 })
|
||||||
|
debug('searchCompound: search query result', search)
|
||||||
if (!search.data?.Page?.media) return []
|
if (!search.data?.Page?.media) return []
|
||||||
return Object.entries(searchResults).map(([filename, id]) => [filename, search.data!.Page!.media!.find(media => media?.id === id)]) as Array<[string, Media | undefined]>
|
return Object.entries(searchResults).map(([filename, id]) => [filename, search.data!.Page!.media!.find(media => media?.id === id)]) as Array<[string, Media | undefined]>
|
||||||
}
|
}
|
||||||
|
|
@ -181,43 +203,53 @@ class AnilistClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleFav (id: number) {
|
async toggleFav (id: number) {
|
||||||
|
debug('toggleFav: toggling favourite for ID', id)
|
||||||
return await this.client.mutation(ToggleFavourite, { id })
|
return await this.client.mutation(ToggleFavourite, { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEntry (media: Media) {
|
async deleteEntry (media: Media) {
|
||||||
|
debug('deleteEntry: deleting entry for media', media)
|
||||||
if (!media.mediaListEntry?.id) return
|
if (!media.mediaListEntry?.id) return
|
||||||
return await this.client.mutation(DeleteEntry, { id: media.mediaListEntry.id })
|
return await this.client.mutation(DeleteEntry, { id: media.mediaListEntry.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async entry (variables: VariablesOf<typeof Entry>) {
|
async entry (variables: VariablesOf<typeof Entry>) {
|
||||||
|
debug('entry: updating entry for media', variables)
|
||||||
return await this.client.mutation(Entry, variables)
|
return await this.client.mutation(Entry, variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
async single (id: number, requestPolicy: RequestPolicy = 'cache-first') {
|
async single (id: number, requestPolicy: RequestPolicy = 'cache-first') {
|
||||||
|
debug('single: fetching media with ID', id)
|
||||||
return await this.client.query(IDMedia, { id }, { requestPolicy })
|
return await this.client.query(IDMedia, { id }, { requestPolicy })
|
||||||
}
|
}
|
||||||
|
|
||||||
following (animeID: number) {
|
following (animeID: number) {
|
||||||
|
debug('following: fetching following for anime with ID', animeID)
|
||||||
return queryStore({ client: this.client, query: Following, variables: { id: animeID } })
|
return queryStore({ client: this.client, query: Following, variables: { id: animeID } })
|
||||||
}
|
}
|
||||||
|
|
||||||
threads (animeID: number, page = 1) {
|
threads (animeID: number, page = 1) {
|
||||||
|
debug('threads: fetching threads for anime with ID', animeID, 'on page', page)
|
||||||
return queryStore({ client: this.client, query: Threads, variables: { id: animeID, page, perPage: 16 } })
|
return queryStore({ client: this.client, query: Threads, variables: { id: animeID, page, perPage: 16 } })
|
||||||
}
|
}
|
||||||
|
|
||||||
comments (threadId: number, page = 1) {
|
comments (threadId: number, page = 1) {
|
||||||
|
debug('comments: fetching comments for thread with ID', threadId, 'on page', page)
|
||||||
return queryStore({ client: this.client, query: Comments, variables: { threadId, page } })
|
return queryStore({ client: this.client, query: Comments, variables: { threadId, page } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleLike (id: number, type: 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY', wasLiked: boolean) {
|
async toggleLike (id: number, type: 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY', wasLiked: boolean) {
|
||||||
|
debug('toggleLike: toggling like for ID', id, 'type', type, 'wasLiked', wasLiked)
|
||||||
return await this.client.mutation(ToggleLike, { id, type, wasLiked })
|
return await this.client.mutation(ToggleLike, { id, type, wasLiked })
|
||||||
}
|
}
|
||||||
|
|
||||||
async comment (variables: VariablesOf<typeof SaveThreadComment> & { rootCommentId?: number }) {
|
async comment (variables: VariablesOf<typeof SaveThreadComment> & { rootCommentId?: number }) {
|
||||||
|
debug('comment: saving comment for thread', variables)
|
||||||
return await this.client.mutation(SaveThreadComment, variables)
|
return await this.client.mutation(SaveThreadComment, variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteComment (id: number, rootCommentId: number) {
|
async deleteComment (id: number, rootCommentId: number) {
|
||||||
|
debug('deleteComment: deleting comment with ID', id, 'rootCommentId', rootCommentId)
|
||||||
return await this.client.mutation(DeleteThreadComment, { id, rootCommentId })
|
return await this.client.mutation(DeleteThreadComment, { id, rootCommentId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { offlineExchange } from '@urql/exchange-graphcache'
|
||||||
import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'
|
import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'
|
||||||
import { Client, fetchExchange } from '@urql/svelte'
|
import { Client, fetchExchange } from '@urql/svelte'
|
||||||
import Bottleneck from 'bottleneck'
|
import Bottleneck from 'bottleneck'
|
||||||
|
import Debug from 'debug'
|
||||||
import { writable as _writable } from 'simple-store-svelte'
|
import { writable as _writable } from 'simple-store-svelte'
|
||||||
import { toast } from 'svelte-sonner'
|
import { toast } from 'svelte-sonner'
|
||||||
|
|
||||||
|
|
@ -17,6 +18,8 @@ import { dev } from '$app/environment'
|
||||||
import native from '$lib/modules/native'
|
import native from '$lib/modules/native'
|
||||||
import { safeLocalStorage, sleep } from '$lib/utils'
|
import { safeLocalStorage, sleep } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:urql')
|
||||||
|
|
||||||
interface ViewerData { viewer: ResultOf<typeof Viewer>['Viewer'], token: string, expires: string }
|
interface ViewerData { viewer: ResultOf<typeof Viewer>['Viewer'], token: string, expires: string }
|
||||||
|
|
||||||
class FetchError extends Error {
|
class FetchError extends Error {
|
||||||
|
|
@ -36,6 +39,10 @@ export const storage = makeDefaultStorage({
|
||||||
maxAge: 31 // The maximum age of the persisted data in days
|
maxAge: 31 // The maximum age of the persisted data in days
|
||||||
})
|
})
|
||||||
|
|
||||||
|
storagePromise.promise.finally(() => {
|
||||||
|
debug('Graphcache storage initialized')
|
||||||
|
})
|
||||||
|
|
||||||
export default new class URQLClient extends Client {
|
export default new class URQLClient extends Client {
|
||||||
limiter = new Bottleneck({
|
limiter = new Bottleneck({
|
||||||
reservoir: 90,
|
reservoir: 90,
|
||||||
|
|
@ -52,12 +59,14 @@ export default new class URQLClient extends Client {
|
||||||
// await sleep(1000)
|
// await sleep(1000)
|
||||||
const res = await fetch(req, opts)
|
const res = await fetch(req, opts)
|
||||||
if (!res.ok && (res.status === 429 || res.status === 500)) {
|
if (!res.ok && (res.status === 429 || res.status === 500)) {
|
||||||
|
debug('Rate limit exceeded', res)
|
||||||
throw new FetchError(res)
|
throw new FetchError(res)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
|
|
||||||
async token () {
|
async token () {
|
||||||
|
debug('Requesting Anilist token')
|
||||||
const res = await native.authAL(`https://anilist.co/api/v2/oauth/authorize?client_id=${dev ? 26159 : 3461}&response_type=token`)
|
const res = await native.authAL(`https://anilist.co/api/v2/oauth/authorize?client_id=${dev ? 26159 : 3461}&response_type=token`)
|
||||||
const token = res.access_token
|
const token = res.access_token
|
||||||
const expires = '' + (Date.now() + (parseInt(res.expires_in) * 1000))
|
const expires = '' + (Date.now() + (parseInt(res.expires_in) * 1000))
|
||||||
|
|
@ -66,12 +75,14 @@ export default new class URQLClient extends Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
async auth (oauth = this.token()) {
|
async auth (oauth = this.token()) {
|
||||||
|
debug('Authenticating Anilist')
|
||||||
const { token, expires } = await oauth
|
const { token, expires } = await oauth
|
||||||
const viewerRes = await this.query(Viewer, {}, { fetchOptions: { headers: { Authorization: `Bearer ${token}` } } })
|
const viewerRes = await this.query(Viewer, {}, { fetchOptions: { headers: { Authorization: `Bearer ${token}` } } })
|
||||||
if (!viewerRes.data?.Viewer) throw new Error('Failed to fetch viewer data')
|
if (!viewerRes.data?.Viewer) throw new Error('Failed to fetch viewer data')
|
||||||
|
|
||||||
this.viewer.value = { viewer: viewerRes.data.Viewer, token, expires }
|
this.viewer.value = { viewer: viewerRes.data.Viewer, token, expires }
|
||||||
localStorage.setItem('ALViewer', JSON.stringify(this.viewer.value))
|
localStorage.setItem('ALViewer', JSON.stringify(this.viewer.value))
|
||||||
|
debug('Anilist viewer data', this.viewer.value.viewer)
|
||||||
|
|
||||||
const lists = viewerRes.data.Viewer.mediaListOptions?.animeList?.customLists ?? []
|
const lists = viewerRes.data.Viewer.mediaListOptions?.animeList?.customLists ?? []
|
||||||
if (!lists.includes('Watched using Hayase')) {
|
if (!lists.includes('Watched using Hayase')) {
|
||||||
|
|
@ -80,12 +91,14 @@ export default new class URQLClient extends Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout () {
|
async logout () {
|
||||||
|
debug('Logging out from Anilist')
|
||||||
await storage.clear()
|
await storage.clear()
|
||||||
localStorage.removeItem('ALViewer')
|
localStorage.removeItem('ALViewer')
|
||||||
native.restart()
|
native.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
setRateLimit (sec: number) {
|
setRateLimit (sec: number) {
|
||||||
|
debug('Setting rate limit', sec)
|
||||||
toast.error('Anilist Error', { description: 'Rate limit exceeded, retrying in ' + Math.round(sec / 1000) + ' seconds.' })
|
toast.error('Anilist Error', { description: 'Rate limit exceeded, retrying in ' + Math.round(sec / 1000) + ' seconds.' })
|
||||||
this.rateLimitPromise ??= sleep(sec).then(() => { this.rateLimitPromise = null })
|
this.rateLimitPromise ??= sleep(sec).then(() => { this.rateLimitPromise = null })
|
||||||
return sec
|
return sec
|
||||||
|
|
@ -106,6 +119,7 @@ export default new class URQLClient extends Client {
|
||||||
updates: {
|
updates: {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
ToggleFavourite (result: ResultOf<typeof ToggleFavourite>, args, cache) {
|
ToggleFavourite (result: ResultOf<typeof ToggleFavourite>, args, cache) {
|
||||||
|
debug('cache update ToggleFavourite', result, args)
|
||||||
if (!result.ToggleFavourite?.anime?.nodes) return result
|
if (!result.ToggleFavourite?.anime?.nodes) return result
|
||||||
const id = args.animeId as number
|
const id = args.animeId as number
|
||||||
|
|
||||||
|
|
@ -115,8 +129,10 @@ export default new class URQLClient extends Client {
|
||||||
cache.writeFragment(gql('fragment Med on Media {id, isFavourite}'), { id, isFavourite: !!exists })
|
cache.writeFragment(gql('fragment Med on Media {id, isFavourite}'), { id, isFavourite: !!exists })
|
||||||
},
|
},
|
||||||
DeleteMediaListEntry: (_, { id }, cache) => {
|
DeleteMediaListEntry: (_, { id }, cache) => {
|
||||||
|
debug('cache update DeleteMediaListEntry', id)
|
||||||
cache.writeFragment(FullMediaList, { id: id as number, progress: null, repeat: null, status: null, customLists: null, score: null })
|
cache.writeFragment(FullMediaList, { id: id as number, progress: null, repeat: null, status: null, customLists: null, score: null })
|
||||||
cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => {
|
cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => {
|
||||||
|
debug('cache update DeleteMediaListEntry, UserLists', data)
|
||||||
if (!data?.MediaListCollection?.lists) return data
|
if (!data?.MediaListCollection?.lists) return data
|
||||||
const oldLists = data.MediaListCollection.lists
|
const oldLists = data.MediaListCollection.lists
|
||||||
|
|
||||||
|
|
@ -132,20 +148,24 @@ export default new class URQLClient extends Client {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
SaveMediaListEntry: (result: ResultOf<typeof Entry>, { mediaId }, cache) => {
|
SaveMediaListEntry: (result: ResultOf<typeof Entry>, { mediaId }, cache) => {
|
||||||
|
debug('cache update SaveMediaListEntry', result, mediaId)
|
||||||
const media = gql('fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}')
|
const media = gql('fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}')
|
||||||
|
|
||||||
const entry = result.SaveMediaListEntry
|
const entry = result.SaveMediaListEntry
|
||||||
|
|
||||||
if (entry?.customLists) entry.customLists = (entry.customLists as string[]).map(name => ({ enabled: true, name }))
|
if (entry?.customLists) entry.customLists = (entry.customLists as string[]).map(name => ({ enabled: true, name }))
|
||||||
|
debug('SaveMediaListEntry entry', entry)
|
||||||
cache.writeFragment(media, {
|
cache.writeFragment(media, {
|
||||||
id: mediaId as number,
|
id: mediaId as number,
|
||||||
mediaListEntry: entry ?? null
|
mediaListEntry: entry ?? null
|
||||||
})
|
})
|
||||||
cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => {
|
cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => {
|
||||||
|
debug('cache update SaveMediaListEntry, UserLists', data)
|
||||||
if (!data?.MediaListCollection?.lists) return data
|
if (!data?.MediaListCollection?.lists) return data
|
||||||
const oldLists = data.MediaListCollection.lists
|
const oldLists = data.MediaListCollection.lists
|
||||||
const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId) ?? { id: -1, media: cache.readFragment(FullMedia, { id: mediaId as number, __typename: 'Media' }) }
|
const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId) ?? { id: -1, media: cache.readFragment(FullMedia, { id: mediaId as number, __typename: 'Media' }) }
|
||||||
if (!oldEntry.media) return data
|
if (!oldEntry.media) return data
|
||||||
|
debug('oldEntry', oldEntry)
|
||||||
|
|
||||||
const lists = oldLists.map(list => {
|
const lists = oldLists.map(list => {
|
||||||
if (!list?.entries) return list
|
if (!list?.entries) return list
|
||||||
|
|
@ -154,6 +174,7 @@ export default new class URQLClient extends Client {
|
||||||
entries: list.entries.filter(entry => entry?.media?.id !== mediaId)
|
entries: list.entries.filter(entry => entry?.media?.id !== mediaId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
debug('lists', lists)
|
||||||
|
|
||||||
const status = result.SaveMediaListEntry?.status ?? oldEntry.media.mediaListEntry?.status ?? 'PLANNING' as const
|
const status = result.SaveMediaListEntry?.status ?? oldEntry.media.mediaListEntry?.status ?? 'PLANNING' as const
|
||||||
|
|
||||||
|
|
@ -163,12 +184,14 @@ export default new class URQLClient extends Client {
|
||||||
lists.push(fallback)
|
lists.push(fallback)
|
||||||
targetList = fallback
|
targetList = fallback
|
||||||
}
|
}
|
||||||
|
debug('targetList', targetList)
|
||||||
targetList.entries ??= []
|
targetList.entries ??= []
|
||||||
targetList.entries.push(oldEntry)
|
targetList.entries.push(oldEntry)
|
||||||
return { ...data, MediaListCollection: { ...data.MediaListCollection, lists } }
|
return { ...data, MediaListCollection: { ...data.MediaListCollection, lists } }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
SaveThreadComment: (_result, args, cache, _info) => {
|
SaveThreadComment: (_result, args, cache, _info) => {
|
||||||
|
debug('cache update SaveThreadComment', args)
|
||||||
if (_info.variables.rootCommentId) {
|
if (_info.variables.rootCommentId) {
|
||||||
const id = _info.variables.rootCommentId as number
|
const id = _info.variables.rootCommentId as number
|
||||||
cache.invalidate({
|
cache.invalidate({
|
||||||
|
|
@ -180,6 +203,7 @@ export default new class URQLClient extends Client {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DeleteThreadComment: (_result, args, cache, _info) => {
|
DeleteThreadComment: (_result, args, cache, _info) => {
|
||||||
|
debug('cache update DeleteThreadComment', args)
|
||||||
const id = (_info.variables.rootCommentId ?? args.id) as number
|
const id = (_info.variables.rootCommentId ?? args.id) as number
|
||||||
cache.invalidate({
|
cache.invalidate({
|
||||||
__typename: 'ThreadComment',
|
__typename: 'ThreadComment',
|
||||||
|
|
@ -196,6 +220,7 @@ export default new class URQLClient extends Client {
|
||||||
},
|
},
|
||||||
optimistic: {
|
optimistic: {
|
||||||
ToggleFavourite ({ animeId }, cache, info) {
|
ToggleFavourite ({ animeId }, cache, info) {
|
||||||
|
debug('optimistic ToggleFavourite', animeId)
|
||||||
const id = animeId as number
|
const id = animeId as number
|
||||||
const media = cache.readFragment(FullMedia, { id, __typename: 'Media' })
|
const media = cache.readFragment(FullMedia, { id, __typename: 'Media' })
|
||||||
info.partial = true
|
info.partial = true
|
||||||
|
|
@ -210,13 +235,16 @@ export default new class URQLClient extends Client {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DeleteMediaListEntry () {
|
DeleteMediaListEntry () {
|
||||||
|
debug('optimistic DeleteMediaListEntry')
|
||||||
return { deleted: true, __typename: 'Deleted' }
|
return { deleted: true, __typename: 'Deleted' }
|
||||||
},
|
},
|
||||||
SaveMediaListEntry (args, cache, info) {
|
SaveMediaListEntry (args, cache, info) {
|
||||||
|
debug('optimistic SaveMediaListEntry', args)
|
||||||
const id = args.mediaId as number
|
const id = args.mediaId as number
|
||||||
const media = cache.readFragment(FullMedia, { id, __typename: 'Media' })
|
const media = cache.readFragment(FullMedia, { id, __typename: 'Media' })
|
||||||
if (!media) return null
|
if (!media) return null
|
||||||
info.partial = true
|
info.partial = true
|
||||||
|
debug('optimistic SaveMediaListEntry media', media)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'PLANNING' as const,
|
status: 'PLANNING' as const,
|
||||||
|
|
@ -232,6 +260,7 @@ export default new class URQLClient extends Client {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ToggleLikeV2 ({ id, type }, cache, info) {
|
ToggleLikeV2 ({ id, type }, cache, info) {
|
||||||
|
debug('optimistic ToggleLikeV2', id, type)
|
||||||
const threadOrCommentId = id as number
|
const threadOrCommentId = id as number
|
||||||
const likable = type as 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY'
|
const likable = type as 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY'
|
||||||
|
|
||||||
|
|
@ -240,6 +269,7 @@ export default new class URQLClient extends Client {
|
||||||
const likableUnion = cache.readFragment(likable === 'THREAD' ? ThreadFrag : CommentFrag, { id: threadOrCommentId, __typename: typename })
|
const likableUnion = cache.readFragment(likable === 'THREAD' ? ThreadFrag : CommentFrag, { id: threadOrCommentId, __typename: typename })
|
||||||
|
|
||||||
if (!likableUnion) return null
|
if (!likableUnion) return null
|
||||||
|
debug('optimistic ToggleLikeV2 likableUnion', likableUnion)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: threadOrCommentId,
|
id: threadOrCommentId,
|
||||||
|
|
@ -295,6 +325,7 @@ export default new class URQLClient extends Client {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.limiter.on('failed', async (error: FetchError | Error, jobInfo) => {
|
this.limiter.on('failed', async (error: FetchError | Error, jobInfo) => {
|
||||||
|
debug('Bottleneck onfailed', error, jobInfo)
|
||||||
// urql has some weird bug that first error is always an AbortError ???
|
// urql has some weird bug that first error is always an AbortError ???
|
||||||
if (error.name === 'AbortError') return undefined
|
if (error.name === 'AbortError') return undefined
|
||||||
if (jobInfo.retryCount > 8) return undefined
|
if (jobInfo.retryCount > 8) return undefined
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
import Debug from 'debug'
|
||||||
|
|
||||||
import type { AnimeThemesResponse } from './types'
|
import type { AnimeThemesResponse } from './types'
|
||||||
|
|
||||||
import { safefetch } from '$lib/utils'
|
import { safefetch } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:animethemes')
|
||||||
|
|
||||||
export function themes (id: number, _fetch = fetch) {
|
export function themes (id: number, _fetch = fetch) {
|
||||||
|
debug('fetching themes for id', id)
|
||||||
return safefetch<AnimeThemesResponse>(_fetch, `https://api.animethemes.moe/anime/?fields[audio]=id,basename,link,size&fields[video]=id,basename,link,tags&filter[external_id]=${id}&filter[has]=resources&filter[site]=AniList&include=animethemes.animethemeentries.videos,animethemes.song,animethemes.song.artists`)
|
return safefetch<AnimeThemesResponse>(_fetch, `https://api.animethemes.moe/anime/?fields[audio]=id,basename,link,size&fields[video]=id,basename,link,tags&filter[external_id]=${id}&filter[has]=resources&filter[site]=AniList&include=animethemes.animethemeentries.videos,animethemes.song,animethemes.song.artists`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
|
import Debug from 'debug'
|
||||||
|
|
||||||
import type { EpisodesResponse, MappingsResponse } from './types'
|
import type { EpisodesResponse, MappingsResponse } from './types'
|
||||||
|
|
||||||
import { safefetch } from '$lib/utils'
|
import { safefetch } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:anizip')
|
||||||
|
|
||||||
// const episodes = safefetch<EpisodesResponse>(`https://hayase.ani.zip/v1/episodes?anilist_id=${params.id}`)
|
// const episodes = safefetch<EpisodesResponse>(`https://hayase.ani.zip/v1/episodes?anilist_id=${params.id}`)
|
||||||
// const mappings = safefetch<MappingsResponse>(fetch, `https://hayase.ani.zip/v1/mappings?anilist_id=${params.id}`)
|
// const mappings = safefetch<MappingsResponse>(fetch, `https://hayase.ani.zip/v1/mappings?anilist_id=${params.id}`)
|
||||||
|
|
||||||
export async function episodes (id: number, _fetch = fetch) {
|
export async function episodes (id: number, _fetch = fetch) {
|
||||||
|
debug('fetching episodes for id', id)
|
||||||
return await safefetch<EpisodesResponse>(_fetch, `https://hayase.ani.zip/v1/episodes?anilist_id=${id}`)
|
return await safefetch<EpisodesResponse>(_fetch, `https://hayase.ani.zip/v1/episodes?anilist_id=${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mappings (id: number, _fetch = fetch) {
|
export async function mappings (id: number, _fetch = fetch) {
|
||||||
|
debug('fetching mappings for id', id)
|
||||||
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?anilist_id=${id}`)
|
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?anilist_id=${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mappingsByKitsuId (kitsuId: number, _fetch = fetch) {
|
export async function mappingsByKitsuId (kitsuId: number, _fetch = fetch) {
|
||||||
|
debug('fetching mappings for kitsu id', kitsuId)
|
||||||
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?kitsu_id=${kitsuId}`)
|
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?kitsu_id=${kitsuId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mappingsByMalId (malId: number, _fetch = fetch) {
|
export async function mappingsByMalId (malId: number, _fetch = fetch) {
|
||||||
|
debug('fetching mappings for mal id', malId)
|
||||||
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?mal_id=${malId}`)
|
return await safefetch<MappingsResponse>(_fetch, `https://hayase.ani.zip/v1/mappings?mal_id=${malId}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Debug from 'debug'
|
||||||
import { writable } from 'simple-store-svelte'
|
import { writable } from 'simple-store-svelte'
|
||||||
import { derived, get, readable } from 'svelte/store'
|
import { derived, get, readable } from 'svelte/store'
|
||||||
import { persisted } from 'svelte-persisted-store'
|
import { persisted } from 'svelte-persisted-store'
|
||||||
|
|
@ -13,6 +14,8 @@ import type { ResultOf, VariablesOf } from 'gql.tada'
|
||||||
|
|
||||||
import { arrayEqual } from '$lib/utils'
|
import { arrayEqual } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:kitsu')
|
||||||
|
|
||||||
const ENDPOINTS = {
|
const ENDPOINTS = {
|
||||||
API_OAUTH: 'https://kitsu.app/api/oauth/token',
|
API_OAUTH: 'https://kitsu.app/api/oauth/token',
|
||||||
API_USER_FETCH: 'https://kitsu.app/api/edge/users',
|
API_USER_FETCH: 'https://kitsu.app/api/edge/users',
|
||||||
|
|
@ -50,6 +53,7 @@ export default new class KitsuSync {
|
||||||
continueIDs = readable<number[]>([], set => {
|
continueIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
const sub = this.userlist.subscribe(values => {
|
const sub = this.userlist.subscribe(values => {
|
||||||
|
debug('continueIDs: checking for IDs')
|
||||||
const entries = Object.entries(values)
|
const entries = Object.entries(values)
|
||||||
if (!entries.length) return []
|
if (!entries.length) return []
|
||||||
|
|
||||||
|
|
@ -61,8 +65,10 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('continueIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('continueIDs: setting IDs', ids)
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
return sub
|
return sub
|
||||||
|
|
@ -71,6 +77,7 @@ export default new class KitsuSync {
|
||||||
planningIDs = readable<number[]>([], set => {
|
planningIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
const sub = this.userlist.subscribe(values => {
|
const sub = this.userlist.subscribe(values => {
|
||||||
|
debug('planningIDs: checking for IDs')
|
||||||
const entries = Object.entries(values)
|
const entries = Object.entries(values)
|
||||||
if (!entries.length) return []
|
if (!entries.length) return []
|
||||||
|
|
||||||
|
|
@ -82,8 +89,10 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('planningIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('planningIDs: setting IDs', ids)
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
return sub
|
return sub
|
||||||
|
|
@ -164,6 +173,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _refresh () {
|
async _refresh () {
|
||||||
|
debug('refreshing Kitsu auth token')
|
||||||
const auth = get(this.auth)
|
const auth = get(this.auth)
|
||||||
const data = await this._post<OAuth>(
|
const data = await this._post<OAuth>(
|
||||||
ENDPOINTS.API_OAUTH,
|
ENDPOINTS.API_OAUTH,
|
||||||
|
|
@ -179,6 +189,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async login (username: string, password: string) {
|
async login (username: string, password: string) {
|
||||||
|
debug('logging in to Kitsu with username', username)
|
||||||
const data = await this._request<OAuth>(
|
const data = await this._request<OAuth>(
|
||||||
ENDPOINTS.API_OAUTH,
|
ENDPOINTS.API_OAUTH,
|
||||||
'POST',
|
'POST',
|
||||||
|
|
@ -190,6 +201,7 @@ export default new class KitsuSync {
|
||||||
)
|
)
|
||||||
|
|
||||||
if ('access_token' in data) {
|
if ('access_token' in data) {
|
||||||
|
debug('Kitsu login successful, setting auth data')
|
||||||
this.auth.set(data)
|
this.auth.set(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +213,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _user () {
|
async _user () {
|
||||||
|
debug('fetching Kitsu user data')
|
||||||
const res = await this._get<Res<User>>(
|
const res = await this._get<Res<User>>(
|
||||||
ENDPOINTS.API_USER_FETCH,
|
ENDPOINTS.API_USER_FETCH,
|
||||||
{
|
{
|
||||||
|
|
@ -219,6 +232,8 @@ export default new class KitsuSync {
|
||||||
|
|
||||||
const { id, attributes } = res.data[0]
|
const { id, attributes } = res.data[0]
|
||||||
|
|
||||||
|
debug('Kitsu user data fetched, setting viewer data')
|
||||||
|
|
||||||
this.viewer.set({
|
this.viewer.set({
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
name: attributes.name ?? '',
|
name: attributes.name ?? '',
|
||||||
|
|
@ -250,6 +265,8 @@ export default new class KitsuSync {
|
||||||
_entriesToML (res: Res<KEntry | User, Anime | Mapping | KEntry | Fav>) {
|
_entriesToML (res: Res<KEntry | User, Anime | Mapping | KEntry | Fav>) {
|
||||||
const entryMap = this.userlist.value
|
const entryMap = this.userlist.value
|
||||||
|
|
||||||
|
debug('Kitsu entries to MediaList conversion started for', res.data.length, 'entries')
|
||||||
|
|
||||||
const { included } = res
|
const { included } = res
|
||||||
|
|
||||||
const relations = {
|
const relations = {
|
||||||
|
|
@ -290,6 +307,8 @@ export default new class KitsuSync {
|
||||||
entryMap[anilistId] = this._kitsuEntryToAl(entry)
|
entryMap[anilistId] = this._kitsuEntryToAl(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('Kitsu entries to MediaList conversion finished, found', Object.keys(entryMap).length, 'entries')
|
||||||
|
|
||||||
for (const [id, fav] of relations.favorites.entries()) {
|
for (const [id, fav] of relations.favorites.entries()) {
|
||||||
const data = fav.relationships!.item!.data as { id: string }
|
const data = fav.relationships!.item!.data as { id: string }
|
||||||
const animeId = data.id
|
const animeId = data.id
|
||||||
|
|
@ -297,6 +316,8 @@ export default new class KitsuSync {
|
||||||
this._getAlId(+animeId)
|
this._getAlId(+animeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('Kitsu favorites loaded, found', Object.keys(this.favorites.value).length, 'favorites')
|
||||||
|
|
||||||
this.userlist.value = entryMap
|
this.userlist.value = entryMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,6 +328,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _makeFavourite (kitsuAnimeId: string) {
|
async _makeFavourite (kitsuAnimeId: string) {
|
||||||
|
debug('making Kitsu favorite for anime ID', kitsuAnimeId)
|
||||||
const viewer = get(this.viewer)
|
const viewer = get(this.viewer)
|
||||||
const data = await this._post<ResSingle<Fav>>(
|
const data = await this._post<ResSingle<Fav>>(
|
||||||
ENDPOINTS.API_FAVOURITES,
|
ENDPOINTS.API_FAVOURITES,
|
||||||
|
|
@ -327,6 +349,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _addEntry (id: string, attributes: Omit<KEntry, 'createdAt' | 'updatedAt'>, alId: number) {
|
async _addEntry (id: string, attributes: Omit<KEntry, 'createdAt' | 'updatedAt'>, alId: number) {
|
||||||
|
debug('adding Kitsu entry for anime ID', id, 'with attributes', attributes)
|
||||||
const viewer = get(this.viewer)
|
const viewer = get(this.viewer)
|
||||||
const data = await this._post<ResSingle<KEntry>>(
|
const data = await this._post<ResSingle<KEntry>>(
|
||||||
ENDPOINTS.API_USER_LIBRARY,
|
ENDPOINTS.API_USER_LIBRARY,
|
||||||
|
|
@ -348,6 +371,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateEntry (id: number, attributes: Omit<KEntry, 'createdAt' | 'updatedAt'>, alId: number) {
|
async _updateEntry (id: number, attributes: Omit<KEntry, 'createdAt' | 'updatedAt'>, alId: number) {
|
||||||
|
debug('updating Kitsu entry for anime ID', id, 'with attributes', attributes)
|
||||||
const data = await this._patch<ResSingle<KEntry>>(
|
const data = await this._patch<ResSingle<KEntry>>(
|
||||||
`${ENDPOINTS.API_USER_LIBRARY}/${id}`,
|
`${ENDPOINTS.API_USER_LIBRARY}/${id}`,
|
||||||
{
|
{
|
||||||
|
|
@ -395,10 +419,12 @@ export default new class KitsuSync {
|
||||||
|
|
||||||
schedule (onList = true) {
|
schedule (onList = true) {
|
||||||
const ids = Object.keys(this.userlist.value).map(id => parseInt(id))
|
const ids = Object.keys(this.userlist.value).map(id => parseInt(id))
|
||||||
|
debug('Kitsu schedule called with onList:', onList, 'and ids:', ids)
|
||||||
return client.schedule(onList && ids.length ? ids : undefined)
|
return client.schedule(onList && ids.length ? ids : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleFav (id: number) {
|
async toggleFav (id: number) {
|
||||||
|
debug('toggling Kitsu favorite for anime ID', id)
|
||||||
const kitsuId = await this._getKitsuId(id)
|
const kitsuId = await this._getKitsuId(id)
|
||||||
if (!kitsuId) {
|
if (!kitsuId) {
|
||||||
toast.error('Kitsu Sync', {
|
toast.error('Kitsu Sync', {
|
||||||
|
|
@ -421,6 +447,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEntry (media: Media) {
|
async deleteEntry (media: Media) {
|
||||||
|
debug('deleting Kitsu entry for media ID', media.id)
|
||||||
const id = this.userlist.value[media.id]?.id
|
const id = this.userlist.value[media.id]?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const res = await this._delete<undefined>(`${ENDPOINTS.API_USER_LIBRARY}/${id}`)
|
const res = await this._delete<undefined>(`${ENDPOINTS.API_USER_LIBRARY}/${id}`)
|
||||||
|
|
@ -446,6 +473,7 @@ export default new class KitsuSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async entry (variables: VariablesOf<typeof Entry>) {
|
async entry (variables: VariablesOf<typeof Entry>) {
|
||||||
|
debug('updating Kitsu entry for media ID', variables.id, 'with variables', variables)
|
||||||
const targetMediaId = variables.id
|
const targetMediaId = variables.id
|
||||||
|
|
||||||
const kitsuEntry = this.userlist.value[targetMediaId]
|
const kitsuEntry = this.userlist.value[targetMediaId]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Debug from 'debug'
|
||||||
import { writable } from 'simple-store-svelte'
|
import { writable } from 'simple-store-svelte'
|
||||||
import { derived, get, readable } from 'svelte/store'
|
import { derived, get, readable } from 'svelte/store'
|
||||||
import { persisted } from 'svelte-persisted-store'
|
import { persisted } from 'svelte-persisted-store'
|
||||||
|
|
@ -14,6 +15,8 @@ import type { ResultOf, VariablesOf } from 'gql.tada'
|
||||||
import { dev } from '$app/environment'
|
import { dev } from '$app/environment'
|
||||||
import { arrayEqual } from '$lib/utils'
|
import { arrayEqual } from '$lib/utils'
|
||||||
|
|
||||||
|
const debug = Debug('ui:mal')
|
||||||
|
|
||||||
type ALMediaStatus = 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING'
|
type ALMediaStatus = 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING'
|
||||||
|
|
||||||
const MAL_TO_AL_STATUS: Record<MALMediaStatus, ALMediaStatus> = {
|
const MAL_TO_AL_STATUS: Record<MALMediaStatus, ALMediaStatus> = {
|
||||||
|
|
@ -109,6 +112,7 @@ export default new class MALSync {
|
||||||
continueIDs = readable<number[]>([], set => {
|
continueIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
const sub = this.userlist.subscribe(values => {
|
const sub = this.userlist.subscribe(values => {
|
||||||
|
debug('continueIDs: checking for IDs')
|
||||||
const entries = Object.entries(values)
|
const entries = Object.entries(values)
|
||||||
if (!entries.length) return []
|
if (!entries.length) return []
|
||||||
|
|
||||||
|
|
@ -120,8 +124,10 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('continueIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('continueIDs: setting new IDs', ids)
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
return sub
|
return sub
|
||||||
|
|
@ -130,6 +136,7 @@ export default new class MALSync {
|
||||||
planningIDs = readable<number[]>([], set => {
|
planningIDs = readable<number[]>([], set => {
|
||||||
let oldvalue: number[] = []
|
let oldvalue: number[] = []
|
||||||
const sub = this.userlist.subscribe(values => {
|
const sub = this.userlist.subscribe(values => {
|
||||||
|
debug('planningIDs: checking for IDs')
|
||||||
const entries = Object.entries(values)
|
const entries = Object.entries(values)
|
||||||
if (!entries.length) return []
|
if (!entries.length) return []
|
||||||
|
|
||||||
|
|
@ -141,8 +148,10 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('planningIDs: found IDs', ids)
|
||||||
if (arrayEqual(oldvalue, ids)) return
|
if (arrayEqual(oldvalue, ids)) return
|
||||||
oldvalue = ids
|
oldvalue = ids
|
||||||
|
debug('planningIDs: setting new IDs', ids)
|
||||||
set(ids)
|
set(ids)
|
||||||
})
|
})
|
||||||
return sub
|
return sub
|
||||||
|
|
@ -232,6 +241,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _refresh () {
|
async _refresh () {
|
||||||
|
debug('Refreshing MAL token')
|
||||||
const auth = get(this.auth)
|
const auth = get(this.auth)
|
||||||
if (!auth?.refresh_token) return
|
if (!auth?.refresh_token) return
|
||||||
|
|
||||||
|
|
@ -252,6 +262,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async login () {
|
async login () {
|
||||||
|
debug('Logging in to MAL')
|
||||||
const state = crypto.randomUUID().replaceAll('-', '')
|
const state = crypto.randomUUID().replaceAll('-', '')
|
||||||
const challenge = (crypto.randomUUID() + crypto.randomUUID()).replaceAll('-', '')
|
const challenge = (crypto.randomUUID() + crypto.randomUUID()).replaceAll('-', '')
|
||||||
const clientID = 'd93b624a92e431a9b6dfe7a66c0c5bbb'
|
const clientID = 'd93b624a92e431a9b6dfe7a66c0c5bbb'
|
||||||
|
|
@ -286,12 +297,15 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _user () {
|
async _user () {
|
||||||
|
debug('Fetching MAL user data')
|
||||||
const res = await this._get<MALUser>(ENDPOINTS.API_USER, {
|
const res = await this._get<MALUser>(ENDPOINTS.API_USER, {
|
||||||
fields: 'anime_statistics'
|
fields: 'anime_statistics'
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('error' in res) return
|
if ('error' in res) return
|
||||||
|
|
||||||
|
debug('MAL user data fetched successfully', res)
|
||||||
|
|
||||||
this.viewer.set({
|
this.viewer.set({
|
||||||
id: res.id,
|
id: res.id,
|
||||||
name: res.name,
|
name: res.name,
|
||||||
|
|
@ -319,6 +333,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadUserList () {
|
async _loadUserList () {
|
||||||
|
debug('Loading MAL user list')
|
||||||
const entryMap: Record<string, ResultOf<typeof FullMediaList>> = {}
|
const entryMap: Record<string, ResultOf<typeof FullMediaList>> = {}
|
||||||
|
|
||||||
let hasNextPage = true
|
let hasNextPage = true
|
||||||
|
|
@ -345,6 +360,8 @@ export default new class MALSync {
|
||||||
|
|
||||||
const ids = data.map(item => item.node.id)
|
const ids = data.map(item => item.node.id)
|
||||||
|
|
||||||
|
debug('MAL user list loaded with', data.length, 'entries and IDs:', ids)
|
||||||
|
|
||||||
const malToAl = await client.malIdsCompound(ids)
|
const malToAl = await client.malIdsCompound(ids)
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
const malId = item.node.id
|
const malId = item.node.id
|
||||||
|
|
@ -358,6 +375,8 @@ export default new class MALSync {
|
||||||
entryMap[alId] = this._malEntryToAl(item.node.my_list_status, item.node.id)
|
entryMap[alId] = this._malEntryToAl(item.node.my_list_status, item.node.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('MAL user list entries mapped to AL IDs:', Object.keys(entryMap))
|
||||||
|
|
||||||
this.userlist.set(entryMap)
|
this.userlist.set(entryMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,6 +422,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
profile (): ResultOf<typeof UserFrag> | undefined {
|
profile (): ResultOf<typeof UserFrag> | undefined {
|
||||||
|
debug('Fetching MAL user profile')
|
||||||
return get(this.viewer)
|
return get(this.viewer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,6 +430,7 @@ export default new class MALSync {
|
||||||
|
|
||||||
schedule (onList = true) {
|
schedule (onList = true) {
|
||||||
const ids = Object.keys(this.userlist.value).map(id => parseInt(id))
|
const ids = Object.keys(this.userlist.value).map(id => parseInt(id))
|
||||||
|
debug('Fetching MAL schedule with IDs:', ids)
|
||||||
return client.schedule(onList && ids.length ? ids : undefined)
|
return client.schedule(onList && ids.length ? ids : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,6 +439,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEntry (media: Media) {
|
async deleteEntry (media: Media) {
|
||||||
|
debug('Deleting MAL entry for media ID', media.id)
|
||||||
const malId = media.idMal ?? await this._getMalId(media.id)
|
const malId = media.idMal ?? await this._getMalId(media.id)
|
||||||
if (!malId) return
|
if (!malId) return
|
||||||
|
|
||||||
|
|
@ -434,6 +456,7 @@ export default new class MALSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async entry (variables: VariablesOf<typeof Entry>) {
|
async entry (variables: VariablesOf<typeof Entry>) {
|
||||||
|
debug('Updating MAL entry for media ID', variables.id, 'with variables', variables)
|
||||||
const targetMediaId = variables.id
|
const targetMediaId = variables.id
|
||||||
const malId = (await client.single(targetMediaId)).data?.Media?.idMal ?? await this._getMalId(targetMediaId)
|
const malId = (await client.single(targetMediaId)).data?.Media?.idMal ?? await this._getMalId(targetMediaId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import anitomyscript, { type AnitomyResult } from 'anitomyscript'
|
import anitomyscript, { type AnitomyResult } from 'anitomyscript'
|
||||||
|
import Debug from 'debug'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
import { dedupeAiring, episodes, isMovie, type Media, getParentForSpecial, isSingleEpisode } from '../anilist'
|
import { dedupeAiring, episodes, isMovie, type Media, getParentForSpecial, isSingleEpisode } from '../anilist'
|
||||||
|
|
@ -29,8 +30,7 @@ if (!('audioTracks' in HTMLVideoElement.prototype)) {
|
||||||
exclusions.push('MULTI')
|
exclusions.push('MULTI')
|
||||||
}
|
}
|
||||||
video.remove()
|
video.remove()
|
||||||
|
const debug = Debug('ui:extensions')
|
||||||
const debug = console.log
|
|
||||||
|
|
||||||
export let fillerEpisodes: Record<number, number[] | undefined> = {}
|
export let fillerEpisodes: Record<number, number[] | undefined> = {}
|
||||||
|
|
||||||
|
|
@ -162,6 +162,7 @@ export const extensions = new class Extensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResultsFromExtensions ({ media, episode, resolution }: { media: Media, episode: number, resolution: keyof typeof videoResolutions }) {
|
async getResultsFromExtensions ({ media, episode, resolution }: { media: Media, episode: number, resolution: keyof typeof videoResolutions }) {
|
||||||
|
debug(`Fetching results for ${media.id}:${media.title?.userPreferred} ${episode} ${resolution}`)
|
||||||
await storage.modules
|
await storage.modules
|
||||||
const workers = storage.workers
|
const workers = storage.workers
|
||||||
if (!Object.values(workers).length) {
|
if (!Object.values(workers).length) {
|
||||||
|
|
@ -199,6 +200,8 @@ export const extensions = new class Extensions {
|
||||||
const checkMovie = !singleEp && movie
|
const checkMovie = !singleEp && movie
|
||||||
const checkBatch = !singleEp && !movie
|
const checkBatch = !singleEp && !movie
|
||||||
|
|
||||||
|
debug(`Checking ${Object.keys(workers).length} extensions for ${media.id}:${media.title?.userPreferred} ${episode} ${resolution} ${checkMovie ? 'movie' : ''} ${checkBatch ? 'batch' : ''}`)
|
||||||
|
|
||||||
for (const [id, worker] of Object.entries(workers)) {
|
for (const [id, worker] of Object.entries(workers)) {
|
||||||
if (!extopts[id]!.enabled) continue
|
if (!extopts[id]!.enabled) continue
|
||||||
if (configs[id]!.type !== 'torrent') continue
|
if (configs[id]!.type !== 'torrent') continue
|
||||||
|
|
@ -229,7 +232,7 @@ export const extensions = new class Extensions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Found ${results.length} results`)
|
debug(`Found ${results.length} results, online ${navigator.onLine}`)
|
||||||
|
|
||||||
const deduped = this.dedupe(results)
|
const deduped = this.dedupe(results)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { releaseProxy, type Remote } from 'abslink'
|
import { releaseProxy, type Remote } from 'abslink'
|
||||||
import { wrap } from 'abslink/w3c'
|
import { wrap } from 'abslink/w3c'
|
||||||
|
import Debug from 'debug'
|
||||||
import { set, getMany, delMany, del } from 'idb-keyval'
|
import { set, getMany, delMany, del } from 'idb-keyval'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import { persisted } from 'svelte-persisted-store'
|
import { persisted } from 'svelte-persisted-store'
|
||||||
|
|
@ -10,6 +11,8 @@ import Worker from './worker?worker'
|
||||||
import type extensionLoader from './worker'
|
import type extensionLoader from './worker'
|
||||||
import type { ExtensionConfig } from 'hayase-extensions'
|
import type { ExtensionConfig } from 'hayase-extensions'
|
||||||
|
|
||||||
|
const debug = Debug('ui:extensions')
|
||||||
|
|
||||||
type SavedExtensions = Record<ExtensionConfig['id'], ExtensionConfig>
|
type SavedExtensions = Record<ExtensionConfig['id'], ExtensionConfig>
|
||||||
|
|
||||||
type ExtensionsOptions = {
|
type ExtensionsOptions = {
|
||||||
|
|
@ -81,6 +84,7 @@ export const storage = new class Storage {
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
saved.subscribe(async value => {
|
saved.subscribe(async value => {
|
||||||
|
debug('saved extensions changed', value)
|
||||||
this.modules = this.load(value)
|
this.modules = this.load(value)
|
||||||
await this.modules
|
await this.modules
|
||||||
this.update(value)
|
this.update(value)
|
||||||
|
|
@ -88,6 +92,7 @@ export const storage = new class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload () {
|
async reload () {
|
||||||
|
debug('reloading extensions')
|
||||||
for (const worker of Object.values(this.workers)) {
|
for (const worker of Object.values(this.workers)) {
|
||||||
worker[releaseProxy]()
|
worker[releaseProxy]()
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +101,7 @@ export const storage = new class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete (id: string) {
|
async delete (id: string) {
|
||||||
|
debug('deleting extension', id)
|
||||||
if (id in this.workers) this.workers[id]![releaseProxy]()
|
if (id in this.workers) this.workers[id]![releaseProxy]()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete this.workers[id]
|
delete this.workers[id]
|
||||||
|
|
@ -108,11 +114,13 @@ export const storage = new class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async import (url: string) {
|
async import (url: string) {
|
||||||
|
debug('importing extension from', url)
|
||||||
const config = await safejson<ExtensionConfig[]>(url)
|
const config = await safejson<ExtensionConfig[]>(url)
|
||||||
if (!config) throw new Error('Make sure the link you provided is a valid JSON config for Hayase', { cause: 'Invalid extension URI' })
|
if (!config) throw new Error('Make sure the link you provided is a valid JSON config for Hayase', { cause: 'Invalid extension URI' })
|
||||||
for (const c of config) {
|
for (const c of config) {
|
||||||
if (!this._validateConfig(c)) throw new Error('Make sure the link you provided is a valid extension config for Hayase', { cause: 'Invalid extension config' })
|
if (!this._validateConfig(c)) throw new Error('Make sure the link you provided is a valid extension config for Hayase', { cause: 'Invalid extension config' })
|
||||||
}
|
}
|
||||||
|
debug('imported config', config)
|
||||||
for (const c of config) {
|
for (const c of config) {
|
||||||
saved.update(value => {
|
saved.update(value => {
|
||||||
value[c.id] = c
|
value[c.id] = c
|
||||||
|
|
@ -127,6 +135,8 @@ export const storage = new class Storage {
|
||||||
|
|
||||||
const modules: Record<string, string> = {}
|
const modules: Record<string, string> = {}
|
||||||
|
|
||||||
|
debug('loading extensions', ids)
|
||||||
|
|
||||||
options.update(options => {
|
options.update(options => {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
if (!(id in options)) {
|
if (!(id in options)) {
|
||||||
|
|
@ -143,12 +153,14 @@ export const storage = new class Storage {
|
||||||
if (module) {
|
if (module) {
|
||||||
modules[id] = module
|
modules[id] = module
|
||||||
} else {
|
} else {
|
||||||
|
debug('loading module', id)
|
||||||
const module = await safejs(config[id]!.code)
|
const module = await safejs(config[id]!.code)
|
||||||
if (!module) continue
|
if (!module) continue
|
||||||
modules[id] = module
|
modules[id] = module
|
||||||
set(id, module)
|
set(id, module)
|
||||||
}
|
}
|
||||||
if (!(id in this.workers)) {
|
if (!(id in this.workers)) {
|
||||||
|
debug('creating worker for', id)
|
||||||
const worker = new Worker({ name: id })
|
const worker = new Worker({ name: id })
|
||||||
const Loader = wrap<typeof extensionLoader>(worker)
|
const Loader = wrap<typeof extensionLoader>(worker)
|
||||||
try {
|
try {
|
||||||
|
|
@ -156,6 +168,7 @@ export const storage = new class Storage {
|
||||||
this.workers[id] = Loader as unknown as Remote<typeof extensionLoader>
|
this.workers[id] = Loader as unknown as Remote<typeof extensionLoader>
|
||||||
await Loader.test()
|
await Loader.test()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debug('failed to load extension', id, e)
|
||||||
// worker.terminate()
|
// worker.terminate()
|
||||||
console.error(e, id)
|
console.error(e, id)
|
||||||
toast.error(`Failed to load extension ${config[id]!.name}`, { description: (e as Error).message })
|
toast.error(`Failed to load extension ${config[id]!.name}`, { description: (e as Error).message })
|
||||||
|
|
@ -168,6 +181,7 @@ export const storage = new class Storage {
|
||||||
async update (config: Record<string, ExtensionConfig>) {
|
async update (config: Record<string, ExtensionConfig>) {
|
||||||
const ids = Object.keys(config)
|
const ids = Object.keys(config)
|
||||||
const configs = Object.values(config)
|
const configs = Object.values(config)
|
||||||
|
debug('updating extensions', ids)
|
||||||
|
|
||||||
const updateURLs = new Set<string>(configs.map(({ update }) => update).filter(e => e != null))
|
const updateURLs = new Set<string>(configs.map(({ update }) => update).filter(e => e != null))
|
||||||
|
|
||||||
|
|
@ -176,6 +190,7 @@ export const storage = new class Storage {
|
||||||
|
|
||||||
const toDelete = ids.filter(id => newconfig[id]?.version !== config[id]!.version)
|
const toDelete = ids.filter(id => newconfig[id]?.version !== config[id]!.version)
|
||||||
if (toDelete.length) {
|
if (toDelete.length) {
|
||||||
|
debug('deleting old extensions', toDelete)
|
||||||
await delMany(toDelete)
|
await delMany(toDelete)
|
||||||
for (const id of toDelete) {
|
for (const id of toDelete) {
|
||||||
if (id in this.workers) {
|
if (id in this.workers) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Debug from 'debug'
|
||||||
import { derived, type Readable } from 'svelte/store'
|
import { derived, type Readable } from 'svelte/store'
|
||||||
import { persisted } from 'svelte-persisted-store'
|
import { persisted } from 'svelte-persisted-store'
|
||||||
|
|
||||||
|
|
@ -5,12 +6,19 @@ import native from '../native'
|
||||||
|
|
||||||
import { defaults } from '.'
|
import { defaults } from '.'
|
||||||
|
|
||||||
|
const _debug = Debug('ui:settings')
|
||||||
|
|
||||||
export const settings = persisted('settings', defaults, { beforeRead: value => ({ ...defaults, ...value }) })
|
export const settings = persisted('settings', defaults, { beforeRead: value => ({ ...defaults, ...value }) })
|
||||||
|
|
||||||
export const debug = persisted('debug', '')
|
export const debug = persisted('debug', '')
|
||||||
|
|
||||||
debug.subscribe((value) => {
|
debug.subscribe((value) => {
|
||||||
native.debug(value)
|
native.debug(value)
|
||||||
|
Debug.enable(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.subscribe((value) => {
|
||||||
|
_debug('settings changed', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
function derivedDeep<T, U> (store: Readable<T>, fn: (value: T) => U) {
|
function derivedDeep<T, U> (store: Readable<T>, fn: (value: T) => U) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Debug from 'debug'
|
||||||
import { readable, writable } from 'simple-store-svelte'
|
import { readable, writable } from 'simple-store-svelte'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import { persisted } from 'svelte-persisted-store'
|
import { persisted } from 'svelte-persisted-store'
|
||||||
|
|
@ -9,6 +10,8 @@ import { w2globby } from '../w2g/lobby'
|
||||||
import type { Media } from '../anilist'
|
import type { Media } from '../anilist'
|
||||||
import type { TorrentFile, TorrentInfo } from 'native'
|
import type { TorrentFile, TorrentInfo } from 'native'
|
||||||
|
|
||||||
|
const debug = Debug('ui:torrent-client')
|
||||||
|
|
||||||
const defaultTorrentInfo: TorrentInfo = {
|
const defaultTorrentInfo: TorrentInfo = {
|
||||||
name: '',
|
name: '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
|
@ -58,7 +61,10 @@ export const server = new class ServerClient {
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
const last = get(this.last)
|
const last = get(this.last)
|
||||||
if (last) this.play(last.id, last.media, last.episode)
|
if (last) {
|
||||||
|
this.play(last.id, last.media, last.episode)
|
||||||
|
debug('restored last torrent', last.id, last.media.title, last.episode)
|
||||||
|
}
|
||||||
|
|
||||||
this.stats.subscribe((stats) => {
|
this.stats.subscribe((stats) => {
|
||||||
native.downloadProgress(stats.progress)
|
native.downloadProgress(stats.progress)
|
||||||
|
|
@ -66,10 +72,12 @@ export const server = new class ServerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cachedSet () {
|
async cachedSet () {
|
||||||
|
debug('fetching cached torrents')
|
||||||
return new Set(await native.cachedTorrents())
|
return new Set(await native.cachedTorrents())
|
||||||
}
|
}
|
||||||
|
|
||||||
play (id: string, media: Media, episode: number) {
|
play (id: string, media: Media, episode: number) {
|
||||||
|
debug('playing torrent', id, media.id, episode)
|
||||||
this.last.set({ id, media, episode })
|
this.last.set({ id, media, episode })
|
||||||
this.active.value = this._play(id, media, episode)
|
this.active.value = this._play(id, media, episode)
|
||||||
w2globby.value?.mediaChange({ episode, mediaId: media.id, torrent: id })
|
w2globby.value?.mediaChange({ episode, mediaId: media.id, torrent: id })
|
||||||
|
|
@ -78,6 +86,7 @@ export const server = new class ServerClient {
|
||||||
|
|
||||||
async _play (id: string, media: Media, episode: number) {
|
async _play (id: string, media: Media, episode: number) {
|
||||||
const result = { id, media, episode, files: await native.playTorrent(id, media.id, episode) }
|
const result = { id, media, episode, files: await native.playTorrent(id, media.id, episode) }
|
||||||
|
debug('torrent play result', result)
|
||||||
this.downloaded.value = this.cachedSet()
|
this.downloaded.value = this.cachedSet()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
428
src/patches/debug.ts
Normal file
428
src/patches/debug.ts
Normal file
|
|
@ -0,0 +1,428 @@
|
||||||
|
/* eslint-env browser */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// patched version of debug because there's actually not a way to disable colors globally!
|
||||||
|
import humanize from 'ms'
|
||||||
|
/**
|
||||||
|
* This is the web browser implementation of `debug()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const exports = {}
|
||||||
|
const module = { exports }
|
||||||
|
|
||||||
|
exports.formatArgs = formatArgs
|
||||||
|
exports.save = save
|
||||||
|
exports.load = load
|
||||||
|
exports.humanize = humanize
|
||||||
|
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 = [6, 2, 3, 4, 5, 1]
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
function useColors () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colorize log arguments if enabled.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatArgs (args) {
|
||||||
|
const { namespace: name, useColors } = this
|
||||||
|
|
||||||
|
if (useColors) {
|
||||||
|
const c = this.color
|
||||||
|
const colorCode = '\u001B[3' + (c < 8 ? c : '8;5;' + c)
|
||||||
|
const prefix = ` ${colorCode};1m${name} \u001B[0m`
|
||||||
|
|
||||||
|
args[0] = prefix + args[0].split('\n').join('\n' + prefix) + ' ' + colorCode + ';1m+' + module.exports.humanize(this.diff) + ' \u001B[0m'
|
||||||
|
} else {
|
||||||
|
args[0] = new Date().toISOString() + ' ' + name + ' ' + args[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.log || console.debug || (() => {})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = humanize
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export default setup(exports)
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.d.ts",
|
"src/**/*.d.ts",
|
||||||
"src/**/*.json",
|
"src/**/*.json",
|
||||||
"src/**/*.svelte"
|
"src/**/*.svelte",
|
||||||
|
"src/patches/debug.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"../node_modules/**"
|
"../node_modules/**"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ export default defineConfig({
|
||||||
'./IORedisConnection': resolve(import.meta.dirname, 'src/patches/empty.cjs'),
|
'./IORedisConnection': resolve(import.meta.dirname, 'src/patches/empty.cjs'),
|
||||||
'./Scripts': resolve(import.meta.dirname, 'src/patches/empty.cjs'),
|
'./Scripts': resolve(import.meta.dirname, 'src/patches/empty.cjs'),
|
||||||
// no exports :/
|
// no exports :/
|
||||||
'bittorrent-tracker/lib/client/websocket-tracker.js': resolve(import.meta.dirname, 'node_modules/bittorrent-tracker/lib/client/websocket-tracker.js')
|
'bittorrent-tracker/lib/client/websocket-tracker.js': resolve(import.meta.dirname, 'node_modules/bittorrent-tracker/lib/client/websocket-tracker.js'),
|
||||||
|
debug: resolve(import.meta.dirname, 'src/patches/debug.ts')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: { port: 7344 },
|
server: { port: 7344 },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue