diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 930f1cc..f04352e 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -46,6 +46,7 @@ type SongRow = { thumbnailUrl?: string; thumbnailWidth?: number; thumbnailHeight?: number; + tidalId: string; }; type AccountAvatarRow = { @@ -333,6 +334,12 @@ function getMigrations(): Migration[] { statement: ` ALTER TABLE songs ADD COLUMN spotifyUrl TEXT NULL; ALTER TABLE songs ADD COLUMN spotifyUri TEXT NULL;` + }, + { + id: 9, + name: 'song tidal id', + statement: ` + ALTER TABLE songs ADD COLUMN tidalId TEXT NULL;` } ]; } @@ -464,8 +471,9 @@ function saveSongInfoData(postUrl: string, songs: SongInfo[]): Promise { for (const song of songs) { db.run( ` - INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, spotifyUrl, spotifyUri, title, artistName, thumbnailUrl, post_url, thumbnailWidth, thumbnailHeight) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, spotifyUrl, spotifyUri, tidalId, + title, artistName, thumbnailUrl, post_url, thumbnailWidth, thumbnailHeight) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ song.postedUrl, @@ -474,6 +482,7 @@ function saveSongInfoData(postUrl: string, songs: SongInfo[]): Promise { song.youtubeUrl, song.spotifyUrl, song.spotifyUri, + song.tidalUri, song.title, song.artistName, song.thumbnailUrl, @@ -574,7 +583,7 @@ function getSongData(postIdsParams: string, postIds: string[]): Promise { db.all( `SELECT post_url, songs.postedUrl, songs.overviewUrl, songs.type, songs.youtubeUrl, songs.spotifyUri, songs.spotifyUri, - songs.title, songs.artistName, songs.thumbnailUrl, songs.post_url, songs.thumbnailWidth, songs.thumbnailHeight + songs.tidalId, songs.title, songs.artistName, songs.thumbnailUrl, songs.post_url, songs.thumbnailWidth, songs.thumbnailHeight FROM songs WHERE post_url IN (${postIdsParams});`, postIds, @@ -591,6 +600,7 @@ function getSongData(postIdsParams: string, postIds: string[]): Promise { + if (!databaseReady) { + await waitReady(); + } + if (!song.tidalUri) { + return false; + } + + const sql = `SELECT songs.title, songs.artistName, songs.tidalId + FROM songs + WHERE songs.tidalId = $tidalId + LIMIT $limit`; + + // If only one exists: This is the one that has just been added + // If more exits: It has been added before + const params = { + $tidalId: song.tidalUri, + $limit: 2 + }; + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err != null) { + logger.error('Error loading songs', err); + reject(err); + return; + } + logger.debug('doesTidalSongExist', song.tidalUri, rows, rows.length > 1); + resolve(rows.length > 1); + }); + }); +} + export async function getPosts( since: string | null, before: string | null, diff --git a/src/lib/server/playlist/tidalPlaylistAdder.ts b/src/lib/server/playlist/tidalPlaylistAdder.ts index 6ab4f14..7818073 100644 --- a/src/lib/server/playlist/tidalPlaylistAdder.ts +++ b/src/lib/server/playlist/tidalPlaylistAdder.ts @@ -6,6 +6,7 @@ import { createHash } from 'crypto'; import { OauthPlaylistAdder } from './oauthPlaylistAdder'; import type { PlaylistAdder } from './playlistAdder'; import type { TidalAddToPlaylistResponse } from './tidalResponse'; +import { doesTidalSongExist } from '$lib/server/db'; export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAdder { private static code_verifier?: string; @@ -117,6 +118,16 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd return; } + const alreadyExists = await doesTidalSongExist(song); + try { + if (alreadyExists) { + this.logger.info('Skip adding song to playlist, has already been added', song); + return; + } + } catch (dbe) { + this.logger.error('Could not check for tidal dupes', dbe); + } + // This would be API v2, but that's still in beta and only allows adding an item *before* another one const options: RequestInit = { method: 'POST', @@ -140,41 +151,6 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`); const request = new Request(apiUrl, options); - // This would be API v1 (or api v2, but *not* the OpenAPI v2), - // but that requires r_usr and w_usr permission scopes which are impossible to request - /* - const options: RequestInit = { - method: 'POST', - headers: { - Authorization: `${token.token_type} ${token.access_token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - onArtifactNotFound: 'SKIP', - trackIds: song.tidalUri, - //toIndex: -1 - onDupes: 'SKIP' - }) - }; - const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/items`); - try { - const r = await fetch(new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}`), { - headers: { - Authorization: `${token.token_type} ${token.access_token}` - } - }); - const txt = await r.text(); - this.logger.debug('playlist', r.status, txt); - const rj = JSON.parse(txt); - this.logger.debug('playlist', rj); - } catch (e) { - this.logger.error('playlist fetch failed', e); - } - - const request = new Request(apiUrl, options); - this.logger.debug('Adding to playlist request', request); - */ - let resp: Response | null = null; let respTxt: string | null = null; try { diff --git a/src/lib/server/timeline.ts b/src/lib/server/timeline.ts index afaef56..944c83d 100644 --- a/src/lib/server/timeline.ts +++ b/src/lib/server/timeline.ts @@ -200,10 +200,14 @@ export class TimelineReader { if (e instanceof Error && e.cause === 429) { this.logger.warn('song.link rate limit reached. Trying again in 10 seconds'); await sleep(10_000); - return await this.getSongInfo(url, remainingTries - 1); + } else { + this.logger.error( + `Failed to load ${url} info from song.link. Trying again in 3 seconds`, + e + ); + await sleep(3_000); } - this.logger.error(`Failed to load ${url} info from song.link`, e); - return null; + return await this.getSongInfo(url, remainingTries - 1); } }