Fix #45 implement crude checking if a song already exists in tidal

This commit is contained in:
2025-07-14 10:59:26 +02:00
parent df35c48e8c
commit 53ee5fabbe
3 changed files with 63 additions and 41 deletions

View File

@ -46,6 +46,7 @@ type SongRow = {
thumbnailUrl?: string; thumbnailUrl?: string;
thumbnailWidth?: number; thumbnailWidth?: number;
thumbnailHeight?: number; thumbnailHeight?: number;
tidalId: string;
}; };
type AccountAvatarRow = { type AccountAvatarRow = {
@ -333,6 +334,12 @@ function getMigrations(): Migration[] {
statement: ` statement: `
ALTER TABLE songs ADD COLUMN spotifyUrl TEXT NULL; ALTER TABLE songs ADD COLUMN spotifyUrl TEXT NULL;
ALTER TABLE songs ADD COLUMN spotifyUri 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<void> {
for (const song of songs) { for (const song of songs) {
db.run( db.run(
` `
INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, spotifyUrl, spotifyUri, title, artistName, thumbnailUrl, post_url, thumbnailWidth, thumbnailHeight) INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, spotifyUrl, spotifyUri, tidalId,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) title, artistName, thumbnailUrl, post_url, thumbnailWidth, thumbnailHeight)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, `,
[ [
song.postedUrl, song.postedUrl,
@ -474,6 +482,7 @@ function saveSongInfoData(postUrl: string, songs: SongInfo[]): Promise<void> {
song.youtubeUrl, song.youtubeUrl,
song.spotifyUrl, song.spotifyUrl,
song.spotifyUri, song.spotifyUri,
song.tidalUri,
song.title, song.title,
song.artistName, song.artistName,
song.thumbnailUrl, song.thumbnailUrl,
@ -574,7 +583,7 @@ function getSongData(postIdsParams: string, postIds: string[]): Promise<Map<stri
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.all( db.all(
`SELECT post_url, songs.postedUrl, songs.overviewUrl, songs.type, songs.youtubeUrl, songs.spotifyUri, songs.spotifyUri, `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 FROM songs
WHERE post_url IN (${postIdsParams});`, WHERE post_url IN (${postIdsParams});`,
postIds, postIds,
@ -591,6 +600,7 @@ function getSongData(postIdsParams: string, postIds: string[]): Promise<Map<stri
youtubeUrl: item.youtubeUrl, youtubeUrl: item.youtubeUrl,
spotifyUrl: item.spotifyUrl, spotifyUrl: item.spotifyUrl,
spotifyUri: item.spotifyUri, spotifyUri: item.spotifyUri,
tidalUri: item.tidalId,
type: item.type, type: item.type,
title: item.title, title: item.title,
artistName: item.artistName, artistName: item.artistName,
@ -683,6 +693,38 @@ function getSongThumbnailData(
}); });
} }
export async function doesTidalSongExist(song: SongInfo): Promise<boolean> {
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( export async function getPosts(
since: string | null, since: string | null,
before: string | null, before: string | null,

View File

@ -6,6 +6,7 @@ import { createHash } from 'crypto';
import { OauthPlaylistAdder } from './oauthPlaylistAdder'; import { OauthPlaylistAdder } from './oauthPlaylistAdder';
import type { PlaylistAdder } from './playlistAdder'; import type { PlaylistAdder } from './playlistAdder';
import type { TidalAddToPlaylistResponse } from './tidalResponse'; import type { TidalAddToPlaylistResponse } from './tidalResponse';
import { doesTidalSongExist } from '$lib/server/db';
export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAdder { export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAdder {
private static code_verifier?: string; private static code_verifier?: string;
@ -117,6 +118,16 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
return; 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 // This would be API v2, but that's still in beta and only allows adding an item *before* another one
const options: RequestInit = { const options: RequestInit = {
method: 'POST', 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 apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
const request = new Request(apiUrl, options); 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 resp: Response | null = null;
let respTxt: string | null = null; let respTxt: string | null = null;
try { try {

View File

@ -200,10 +200,14 @@ export class TimelineReader {
if (e instanceof Error && e.cause === 429) { if (e instanceof Error && e.cause === 429) {
this.logger.warn('song.link rate limit reached. Trying again in 10 seconds'); this.logger.warn('song.link rate limit reached. Trying again in 10 seconds');
await sleep(10_000); 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 await this.getSongInfo(url, remainingTries - 1);
return null;
} }
} }