wip: initial kitsu impl

This commit is contained in:
ThaUnknown 2025-05-18 15:09:08 +02:00
parent 7ba810057e
commit db2b0a738a
No known key found for this signature in database
8 changed files with 212 additions and 7 deletions

View file

@ -4,8 +4,9 @@
"dbaeumer.vscode-eslint",
"GraphQL.vscode-graphql-syntax",
"YoavBls.pretty-ts-errors",
"svelte.svelte-vscode",
"svelte.svelte-vscode", // 109.5.2 or older NOT NEWER
"ardenivanov.svelte-intellisense",
"Gruntfuggly.todo-tree"
"Gruntfuggly.todo-tree",
"bradlc.vscode-tailwindcss"
]
}

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.3.15",
"version": "6.3.16",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",

View file

@ -27,7 +27,7 @@
ident = { nick: 'Guest-' + crypto.randomUUID().slice(0, 6), id: crypto.randomUUID().slice(0, 6), pfpid: '0', type: 'guest' }
}
if (!irc.value) irc.value = MessageClient.new(ident)
irc.value ??= MessageClient.new(ident)
let message = ''
let rows = 1

View file

@ -159,7 +159,7 @@ export function normalizeTracks (_tracks: Track[]) {
}
})
return lang.reduce<Record<string, typeof lang>>((acc, track) => {
if (!acc[track.language]) acc[track.language] = []
acc[track.language] ??= []
acc[track.language]!.push(track)
return acc
}, {})
@ -174,7 +174,7 @@ export function normalizeSubs (_tracks?: Record<number | string, { meta: { langu
name: meta.name ?? meta.language ?? (!hasEng ? 'eng' : 'unk')
}))
return lang.reduce<Record<string, typeof lang>>((acc, track) => {
if (!acc[track.language]) acc[track.language] = []
acc[track.language] ??= []
acc[track.language]!.push(track)
return acc
}, {})

View file

@ -19,7 +19,7 @@ export default new class AuthAggregator {
return () => unsub.forEach(fn => fn())
})
syncSettings = persisted('syncSettings', { al: true, local: true })
syncSettings = persisted('syncSettings', { al: true, local: true, kitsu: true, mal: true })
// AUTH
anilist () {

8
src/lib/modules/auth/kitsu-types.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
export interface KitsuOAuth {
access_token: string
created_at: number
expires_in: number // Seconds until the access_token expires (30 days default)
refresh_token: string
scope: string
token_type: string
}

View file

@ -0,0 +1,195 @@
import { readable, writable } from 'simple-store-svelte'
import type { Media } from '../anilist'
import type { KitsuOAuth } from './kitsu-types'
import type { Entry } from '../anilist/queries'
import type { VariablesOf } from 'gql.tada'
import { safeLocalStorage } from '$lib/utils'
const ENDPOINTS = {
API_OAUTH: 'https://kitsu.app/api/oauth/token',
API_USER_FETCH: 'https://kitsu.app/api/edge/users',
API_USER_LIBRARY: 'https://kitsu.app/api/edge/library-entries'
}
export default new class KitsuSync {
auth = writable<KitsuOAuth | undefined>(safeLocalStorage('kitsuAuth'))
viewer = writable<{id: number} | undefined>(safeLocalStorage('kitsuViewer'))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async _request (url: string, method: string, body?: any): Promise<any> {
try {
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/vnd.api+json',
Accept: 'application/vnd.api+json',
Authorization: this.auth.value ? `Bearer ${this.auth.value.access_token}` : ''
},
body: JSON.stringify(body)
})
if (res.status === 403 && !body?.refresh_token) {
await this._refresh()
return await this._request(url, method, body)
}
return await res.json()
} catch (error) {
// TODO: :^)
const err = error as Error
throw err
}
}
async _refresh () {
try {
const data = await this._request(
ENDPOINTS.API_OAUTH,
'POST',
{
grant_type: 'refresh_token',
refresh_token: this.auth.value?.refresh_token
}
)
this.viewer.value = data
await this._user()
} catch (error) {
this.viewer.value = undefined
}
}
async _login (username: string, password: string) {
const data = await this._request(
ENDPOINTS.API_OAUTH,
'POST',
{
grant_type: 'password',
username,
password
}
)
this.viewer.value = data
await this._user()
}
async _user () {
const data = await this._request(
ENDPOINTS.API_USER_FETCH,
'GET',
{ 'filter[self]': true }
)
const [user] = data
return user.id
}
async _getEntry (id: number) {
const data = await this._request(
ENDPOINTS.API_USER_LIBRARY,
'GET',
{
'filter[animeId]': id,
'filter[userId]': this.viewer.value?.id,
'filter[kind]': 'anime'
}
)
const [anime] = data.data
return anime
}
async _addEntry (id: number, attributes: Record<string, unknown>) {
const data = await this._request(
ENDPOINTS.API_USER_LIBRARY,
'POST',
{
data: {
attributes: {
status: 'planned'
},
relationships: {
anime: {
data: {
id,
type: 'anime'
}
},
user: {
data: {
id: this.viewer.value?.id,
type: 'users'
}
}
},
type: 'library-entries'
}
}
)
return data
}
async _updateEntry (id: number, attributes: Record<string, unknown>) {
const data = await this._request(
`${ENDPOINTS.API_USER_LIBRARY}/${id}`,
'PATCH', {
data: {
id,
attributes,
type: 'library-entries'
}
}
)
return data
}
async _deleteEntry (id: number) {
this._request(
`${ENDPOINTS.API_USER_LIBRARY}/${id}`,
'DELETE'
)
}
hasAuth = readable(false)
id () {
return -1
}
profile () {
}
// QUERIES/MUTATIONS
schedule () {
}
toggleFav (id: number) {
}
delete (id: number) {
}
following (id: number) {
}
planningIDs () {
}
continueIDs () {
}
sequelIDs () {
}
watch (media: Media, progress: number) {
}
entry (variables: VariablesOf<typeof Entry>) {
}
}()

View file

@ -1 +1,2 @@
// eslint-disable-next-line no-undef
module.exports = {}