From 38e8b4c2ba198f46b59a29371c2918b8ec9b29ba Mon Sep 17 00:00:00 2001 From: Max Nuding Date: Sun, 6 Jul 2025 18:43:36 +0200 Subject: [PATCH] prepare for being available on multiple domain names --- src/hooks.server.ts | 24 +++++++++++--- src/lib/log.ts | 12 +++---- src/lib/server/db.ts | 9 ++++++ src/lib/server/playlist/oauthPlaylistAdder.ts | 8 +++++ src/lib/server/playlist/ytPlaylistAdder.ts | 32 ++++++++++++------- src/lib/server/timeline.ts | 26 +++++++++++++++ src/routes/spotifyAuth/+page.server.ts | 5 +-- src/routes/ytauth/+page.server.ts | 6 ++-- 8 files changed, 97 insertions(+), 25 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 87c25d9..2dd94a4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -3,16 +3,32 @@ import { TimelineReader } from '$lib/server/timeline'; import type { Handle, HandleServerError } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; import fs from 'fs/promises'; +import { close } from '$lib/server/db'; const logger = new Logger('App'); -logger.log('App startup'); +if (process?.pid) { + try { + await fs.writeFile('moshing-mammut.pid', process.pid.toString()); + } catch (e) { + logger.error('Could not write PID to file', e); + } +} + +logger.log('App startup, PID', process?.pid); + logger.log('Debug log enabled', Logger.isDebugEnabled()); TimelineReader.init(); -export const handleError = (({ error }) => { - if (error instanceof Error) { - logger.error('Something went wrong: ', error.name, error.message); +process.on('sveltekit:shutdown', (reason) => { + close(); + logger.log('Shutting down', reason); + process.exit(0); +}); + +export const handleError = (({ error, status }) => { + if (error instanceof Error && status !== 404) { + logger.error('Something went wrong:', error.name, error.message); } return { diff --git a/src/lib/log.ts b/src/lib/log.ts index 4959a76..a864622 100644 --- a/src/lib/log.ts +++ b/src/lib/log.ts @@ -48,25 +48,25 @@ export class Logger { if (!enableVerboseLog) { return; } - console.debug(new Date().toISOString(), `- ${this.name} -`, ...params); + console.debug(new Date().toISOString(), `- ${this.name} -`, '- [VRBSE] -', ...params); } public debug(...params: any[]) { if (!Logger.isDebugEnabled()) { return; } - console.debug(new Date().toISOString(), `- ${this.name} -`, ...params); + console.debug(new Date().toISOString(), `- ${this.name} -`, '- [DEBUG] -', ...params); } public log(...params: any[]) { - console.log(new Date().toISOString(), `- ${this.name} -`, ...params); + console.log(new Date().toISOString(), `- ${this.name} -`, '- [ LOG ] -', ...params); } public info(...params: any[]) { - console.info(new Date().toISOString(), `- ${this.name} -`, ...params); + console.info(new Date().toISOString(), `- ${this.name} -`, '- [INFO ] -', ...params); } public warn(...params: any[]) { - console.warn(new Date().toISOString(), `- ${this.name} -`, ...params); + console.warn(new Date().toISOString(), `- ${this.name} -`, '- [WARN ] -', ...params); } public error(...params: any[]) { - console.error(new Date().toISOString(), `- ${this.name} -`, ...params); + console.error(new Date().toISOString(), `- ${this.name} -`, '- [ERROR] -', ...params); } public static error(...params: any[]) { diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 7dc7108..930f1cc 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -68,6 +68,15 @@ type Migration = { }; const db: sqlite3.Database = new sqlite3.Database('moshingmammut.db'); + +export function close() { + try { + db.close(); + } catch (e) { + logger.error('Could not close DB'); + } +} + // for the local masto instance, the instance name is *not* saved // as part of the username or acct, so it needs to be stripped const ignoredUsers: string[] = diff --git a/src/lib/server/playlist/oauthPlaylistAdder.ts b/src/lib/server/playlist/oauthPlaylistAdder.ts index ffc9b1f..edcae93 100644 --- a/src/lib/server/playlist/oauthPlaylistAdder.ts +++ b/src/lib/server/playlist/oauthPlaylistAdder.ts @@ -1,3 +1,4 @@ +import { BASE_URL } from '$env/static/private'; import { Logger } from '$lib/log'; import type { OauthResponse } from '$lib/mastodon/response'; import fs from 'fs/promises'; @@ -6,6 +7,7 @@ export abstract class OauthPlaylistAdder { /// How many minutes before expiry the token will be refreshed protected refresh_time: number = 15; protected logger: Logger = new Logger('OauthPlaylistAdder'); + protected redirectUri?: URL; protected constructor( protected apiBase: string, @@ -22,6 +24,12 @@ export abstract class OauthPlaylistAdder { } } + protected getRedirectUri(suffix: string): URL { + const uri = this.redirectUri ?? new URL(`${BASE_URL}/${suffix}`); + this.logger.debug('getRedirectUri', uri); + return uri; + } + protected constructAuthUrlInternal( endpointUrl: string, clientId: string, diff --git a/src/lib/server/playlist/ytPlaylistAdder.ts b/src/lib/server/playlist/ytPlaylistAdder.ts index e7f1fdc..2330767 100644 --- a/src/lib/server/playlist/ytPlaylistAdder.ts +++ b/src/lib/server/playlist/ytPlaylistAdder.ts @@ -1,9 +1,4 @@ -import { - BASE_URL, - YOUTUBE_CLIENT_ID, - YOUTUBE_CLIENT_SECRET, - YOUTUBE_PLAYLIST_ID -} from '$env/static/private'; +import { YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_PLAYLIST_ID } from '$env/static/private'; import { Logger } from '$lib/log'; import type { OauthResponse } from '$lib/mastodon/response'; import type { SongInfo } from '$lib/odesliResponse'; @@ -17,6 +12,7 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist } public constructAuthUrl(redirectUri: URL): URL { + this.redirectUri = redirectUri; let additionalParameters = new Map([ ['access_type', 'offline'], ['include_granted_scopes', 'false'] @@ -33,6 +29,7 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist public async receivedAuthCode(code: string, url: URL) { this.logger.debug('received code'); + this.redirectUri = url; const tokenUrl = new URL('https://oauth2.googleapis.com/token'); await this.receivedAuthCodeInternal( tokenUrl, @@ -57,13 +54,13 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist } } - private async refreshToken(): Promise { + private async refreshToken(force: boolean = false): Promise { const tokenInfo = await this.shouldRefreshToken(); if (tokenInfo == null) { return null; } let token = tokenInfo.token; - if (!tokenInfo.refresh) { + if (!tokenInfo.refresh && !force) { return token; } if (!token.refresh_token) { @@ -76,12 +73,17 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist tokenUrl, YOUTUBE_CLIENT_ID, token.refresh_token, - `${BASE_URL}/ytauth`, + this.getRedirectUri('ytauth').toString(), YOUTUBE_CLIENT_SECRET ); } - public async addToPlaylist(song: SongInfo) { + public async addToPlaylist(song: SongInfo) {} + + private async addToPlaylistRetry(song: SongInfo, remaning: number = 3) { + if (remaning < 0) { + this.logger.error('max retries reached, song will not be added to spotify playlist'); + } this.logger.debug('addToYoutubePlaylist'); const token = await this.refreshToken(); if (token == null) { @@ -139,7 +141,15 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist const respObj = await resp.json(); this.logger.info('Added to playlist', youtubeId, song.title); if (respObj.error) { - this.logger.debug('Add to playlist failed', respObj.error.errors); + this.logger.error('Add to playlist failed', respObj.error.errors); + if (respObj.error.errors && respObj.error.errors[0].reason === 'authError') { + this.logger.info('Refreshing auth token'); + const token = await this.refreshToken(true); + if (token == null) { + return; + } + this.addToPlaylistRetry(song, remaning--); + } } } } diff --git a/src/lib/server/timeline.ts b/src/lib/server/timeline.ts index 653711a..415975e 100644 --- a/src/lib/server/timeline.ts +++ b/src/lib/server/timeline.ts @@ -2,6 +2,7 @@ import { HASHTAG_FILTER, MASTODON_ACCESS_TOKEN, MASTODON_INSTANCE, + IGNORE_USERS, ODESLI_API_KEY, YOUTUBE_API_KEY } from '$env/static/private'; @@ -48,6 +49,7 @@ export class TimelineReader { private lastPosts: string[] = []; private playlistAdders: PlaylistAdder[]; private logger: Logger; + private ignoredUsers: string[]; private async isMusicVideo(videoId: string) { if (!YOUTUBE_API_KEY || YOUTUBE_API_KEY === 'CHANGE_ME') { @@ -372,6 +374,20 @@ export class TimelineReader { } private async checkAndSavePost(post: Post) { + if (IGNORE_USERS !== undefined && IGNORE_USERS !== '' && IGNORE_USERS !== 'CHANGE_ME') { + const ignorelist = IGNORE_USERS.split(','); + const isIgnored = ignorelist.includes(post.account.username); + const isIgnoredDb = this.ignoredUsers.includes(post.account.username); + this.logger.debug( + 'Check if user', + post.account.username, + 'is ignored', + this.ignoredUsers, + isIgnored, + isIgnoredDb + ); + } + const hashttags: string[] = HASHTAG_FILTER.split(','); const found_tags: Tag[] = post.tags.filter((t: Tag) => hashttags.includes(t.name)); @@ -480,6 +496,16 @@ export class TimelineReader { this.logger = new Logger('Timeline'); this.logger.log('Constructing timeline object'); this.playlistAdders = [new YoutubePlaylistAdder(), new SpotifyPlaylistAdder()]; + this.ignoredUsers = + IGNORE_USERS === undefined + ? [] + : IGNORE_USERS.split(',') + .map((u) => (u.startsWith('@') ? u.substring(1) : u)) + .map((u) => + u.endsWith('@' + MASTODON_INSTANCE) + ? u.substring(0, u.length - ('@' + MASTODON_INSTANCE).length) + : u + ); this.startWebsocket(); this.loadPostsSinceLastRun() diff --git a/src/routes/spotifyAuth/+page.server.ts b/src/routes/spotifyAuth/+page.server.ts index db5630f..b027c3d 100644 --- a/src/routes/spotifyAuth/+page.server.ts +++ b/src/routes/spotifyAuth/+page.server.ts @@ -7,9 +7,10 @@ const { DEV } = import.meta.env; const logger = new Logger('SpotifyAuth'); -export const load: PageServerLoad = async ({ url }) => { +export const load: PageServerLoad = async ({ url, request }) => { + const baseUrl = request.headers.get('X-Forwarded-Host') ?? BASE_URL; const adder = new SpotifyPlaylistAdder(); - let redirect_uri = new URL(`${BASE_URL}/spotifyAuth`); + let redirect_uri = new URL(`${new URL(BASE_URL).protocol}//${baseUrl}/spotifyAuth`); if (url.hostname === 'localhost' && DEV) { redirect_uri.hostname = '127.0.0.1'; } diff --git a/src/routes/ytauth/+page.server.ts b/src/routes/ytauth/+page.server.ts index db72690..cc0ecbc 100644 --- a/src/routes/ytauth/+page.server.ts +++ b/src/routes/ytauth/+page.server.ts @@ -6,9 +6,11 @@ import type { PageServerLoad } from './$types'; const logger = new Logger('YT Auth'); -export const load: PageServerLoad = async ({ url }) => { +export const load: PageServerLoad = async ({ url, request }) => { + const baseUrl = request.headers.get('X-Forwarded-Host') ?? BASE_URL; const adder = new YoutubePlaylistAdder(); - const redirect_uri = new URL(`${BASE_URL}/ytauth`); + logger.debug('redirect URL', `${new URL(BASE_URL).protocol}//${baseUrl}/ytauth`); + const redirect_uri = new URL(`${new URL(BASE_URL).protocol}//${baseUrl}/ytauth`); if (url.searchParams.has('code')) { logger.debug(url.searchParams); await adder.receivedAuthCode(url.searchParams.get('code') || '', redirect_uri);