From c49019187ec02b5683e6dcbfe7f43d2b9ee49a8e Mon Sep 17 00:00:00 2001 From: realies <5107843+realies@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:09:46 +0000 Subject: [PATCH] separate verify timestamps from missing music --- API.md | 60 ++++++++++++++++++++++------ mod.ts | 1 + src/index.ts | 53 ++++++++++++++----------- src/services/getMissingMusic.ts | 50 ----------------------- src/services/verifyTimestamps.ts | 68 ++++++++++++++++++++++++++++++++ src/types.ts | 17 ++++++++ 6 files changed, 164 insertions(+), 85 deletions(-) create mode 100644 src/services/verifyTimestamps.ts diff --git a/API.md b/API.md index 737a5f1..71e5aa0 100644 --- a/API.md +++ b/API.md @@ -80,10 +80,22 @@ Downloads missing tracks from a list of liked tracks. function getMissingMusic( likes: UserLike[], folder?: string, - callbacks?: Callbacks + callbacks?: Callbacks, ): Promise ``` +#### verifyTimestamps(likes, folder, callbacks?) + +Verifies and updates timestamps of existing files to match their like dates. + +```typescript +function verifyTimestamps( + likes: UserLike[], + folder: string, + callbacks?: Callbacks, +): Promise +``` + ### Types ```typescript @@ -167,8 +179,20 @@ interface DownloadResult { success: boolean; /** Any error message if the download failed */ error?: string; - /** Additional status message for successful operations */ - message?: string; + }; +} + +interface VerifyTimestampResult { + /** Title of the verified track */ + track: string; + /** Verification status */ + status: { + /** Whether the timestamp was verified successfully */ + success: boolean; + /** Whether the timestamp needed updating */ + updated: boolean; + /** Any error message if verification failed */ + error?: string; }; } ``` @@ -187,19 +211,31 @@ await soundCloudSync({ }); // Low-level usage with individual functions -import { getClient, getUserLikes, getMissingMusic } from 'soundcloud-sync'; +import { getClient, getUserLikes, getMissingMusic, verifyTimestamps } from 'soundcloud-sync'; const client = await getClient('your-username'); const likes = await getUserLikes(client, '0', 100); -const results = await getMissingMusic(likes, './my-music', { - onDownloadStart: (track) => console.log(`Starting ${track.title}`), - onDownloadComplete: (track) => console.log(`Completed ${track.title}`), - onDownloadError: (track, error) => console.error(`Failed ${track.title}:`, error), + +// Define callbacks for progress tracking +const callbacks = { + onDownloadStart: (track) => console.log(`Starting "${track.title}"`), + onDownloadComplete: (track) => console.log(`Completed "${track.title}"`), + onDownloadError: (track, error) => console.error(`Failed "${track.title}": ${error}`), onTimestampUpdate: (track, oldDate, newDate) => - console.log( - `Updated timestamp for ${track.title} from ${oldDate.toISOString()} to ${newDate.toISOString()}`, - ), -}, true); + console.log(`Updated timestamp for ${track.title} from ${oldDate.toISOString()} to ${newDate.toISOString()}`), +}; + +// Verify timestamps of existing files +const verifyResults = await verifyTimestamps(likes, './my-music', callbacks); +console.log('Updated files:', verifyResults.filter(r => r.status.updated).map(r => r.track)); +console.log('Failed verifications:', verifyResults.filter(r => !r.status.success).map(r => r.track)); + +// Download missing tracks +const results = await getMissingMusic(likes, './my-music', callbacks); + +// Process download results +console.log('Downloaded tracks:', results.filter(r => r.status.success).map(r => r.track)); +console.log('Failed tracks:', results.filter(r => !r.status.success).map(r => r.track)); ``` ```bash diff --git a/mod.ts b/mod.ts index 7c626b8..5da5d83 100644 --- a/mod.ts +++ b/mod.ts @@ -11,3 +11,4 @@ export { default as soundCloudSync } from './src/index.ts'; export { default as getClient } from './src/services/getClient.ts'; export { default as getUserLikes } from './src/services/getUserLikes.ts'; export { default as getMissingMusic } from './src/services/getMissingMusic.ts'; +export { default as verifyTimestamps } from './src/services/verifyTimestamps.ts'; diff --git a/src/index.ts b/src/index.ts index 7561f70..b6abee8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,52 @@ import getClient from './services/getClient.ts'; import getUserLikes from './services/getUserLikes.ts'; import getMissingMusic from './services/getMissingMusic.ts'; +import verifyTimestamps from './services/verifyTimestamps.ts'; import logger from './helpers/logger.ts'; import { SoundCloudSyncOptions } from './types.ts'; -export { getClient, getUserLikes, getMissingMusic }; +export { getClient, getUserLikes, getMissingMusic, verifyTimestamps }; export default async function soundCloudSync({ username, folder = './music', limit = 50, - verifyTimestamps = false, + verifyTimestamps: shouldVerifyTimestamps = false, }: SoundCloudSyncOptions) { logger.info(`Getting latest likes for ${username}`); try { const client = await getClient(username); const userLikes = await getUserLikes(client, '0', limit); - const results = await getMissingMusic( - userLikes, - folder, - { - onDownloadStart: track => logger.info('Downloading track', track.title), - onDownloadComplete: track => logger.info('Added track', track.title), - onDownloadError: (track, error) => - logger.error('Failed to download track', { - title: track.title, - error: error instanceof Error ? error.message : String(error), - }), - onTimestampUpdate: (track, oldDate, newDate) => - logger.info( - `Updated timestamp for ${track.title} from ${oldDate.toISOString()} to ${newDate.toISOString()}`, - ), - }, - verifyTimestamps, + + const callbacks = { + onDownloadStart: track => logger.info(`Downloading "${track.title}"`), + onDownloadComplete: track => logger.info(`Added "${track.title}"`), + onDownloadError: (track, error) => + logger.error( + `Failed to download "${track.title}": ${ + error instanceof Error ? error.message : String(error) + }`, + ), + onTimestampUpdate: (track, oldDate, newDate) => + logger.info( + `Updated timestamp for ${track.title}" from ${oldDate.toISOString()} to ${newDate.toISOString()}`, + ), + }; + + let verifyResultsLength = 0; + if (shouldVerifyTimestamps) { + ({ length: verifyResultsLength } = await verifyTimestamps(userLikes, folder, callbacks)); + } + + const downloadResults = await getMissingMusic(userLikes, folder, callbacks); + logger.info( + `Completed successfully: ${downloadResults.length} tracks downloaded${ + shouldVerifyTimestamps ? `, ${verifyResultsLength} tracks verified` : '' + }`, ); - logger.info(`Completed successfully, ${results.length} tracks processed`); } catch (error) { - logger.error('An error occurred', { - error: error instanceof Error ? error.message : String(error), - }); + logger.error(`An error occurred: ${error instanceof Error ? error.message : String(error)}`); throw error; } } diff --git a/src/services/getMissingMusic.ts b/src/services/getMissingMusic.ts index a30ce80..3809a87 100644 --- a/src/services/getMissingMusic.ts +++ b/src/services/getMissingMusic.ts @@ -4,7 +4,6 @@ import { ID3Writer } from 'browser-id3-writer'; import sanitiseFilename from '../helpers/sanitise.ts'; import webAgent from './webAgent.ts'; import { UserLike, Callbacks, DownloadResult, Track } from '../types.ts'; -import logger from '../helpers/logger.ts'; const getBestTranscoding = (track: Track) => track.media.transcodings.find( @@ -39,43 +38,10 @@ const downloadArtwork = async (url: string): Promise => { } }; -const verifyAndUpdateTimestamp = async ( - filePath: string, - created_at: string, - track: Track, - callbacks: Callbacks, -): Promise => { - try { - const stats = await fs.stat(filePath); - const likeDate = new Date(created_at); - const fileDate = stats.mtime; - - if (Math.abs(likeDate.getTime() - fileDate.getTime()) > 1000) { - // 1 second tolerance - logger.debug('Updating timestamp', { - file: path.basename(filePath), - from: fileDate.toISOString(), - to: likeDate.toISOString(), - }); - await fs.utimes(filePath, likeDate, likeDate); - callbacks.onTimestampUpdate?.(track, fileDate, likeDate); - return true; - } - return false; - } catch (error) { - logger.error('Failed to verify/update timestamp', { - file: path.basename(filePath), - error: error instanceof Error ? error.message : String(error), - }); - return false; - } -}; - export default async function getMissingMusic( likes: UserLike[], folder = './music', callbacks: Callbacks = {}, - verifyTimestamps = false, ): Promise { try { await fs.access(folder); @@ -85,21 +51,6 @@ export default async function getMissingMusic( const availableMusic = (await fs.readdir(folder)).map(filename => path.parse(filename).name); - // Handle timestamp verification for existing files first - if (verifyTimestamps) { - const existingTracks = likes.filter(({ track }) => - availableMusic.includes(sanitiseFilename(track.title)), - ); - - // Just verify timestamps, don't collect results since these aren't downloads - await Promise.all( - existingTracks.map(async ({ track, created_at }) => { - const filePath = path.join(folder, `${sanitiseFilename(track.title)}.mp3`); - await verifyAndUpdateTimestamp(filePath, created_at, track, callbacks); - }), - ); - } - // Handle missing tracks in parallel const missingTracks = likes.filter( ({ track }) => @@ -107,7 +58,6 @@ export default async function getMissingMusic( track.media.transcodings.length > 0, ); - // Only return download results return Promise.all( missingTracks.map(async ({ track, created_at }) => { callbacks.onDownloadStart?.(track); diff --git a/src/services/verifyTimestamps.ts b/src/services/verifyTimestamps.ts new file mode 100644 index 0000000..c1ce65e --- /dev/null +++ b/src/services/verifyTimestamps.ts @@ -0,0 +1,68 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { Track, Callbacks, UserLike, VerifyTimestampResult } from '../types.ts'; +import logger from '../helpers/logger.ts'; +import sanitiseFilename from '../helpers/sanitise.ts'; + +const verifyAndUpdateTimestamp = async ( + filePath: string, + created_at: string, + track: Track, + callbacks: Callbacks, +): Promise => { + try { + const stats = await fs.stat(filePath); + const likeDate = new Date(created_at); + const fileDate = stats.mtime; + + if (Math.abs(likeDate.getTime() - fileDate.getTime()) > 1000) { + // 1 second tolerance + logger.debug( + `Verifying timestamp for "${path.basename(filePath)}" from ${fileDate.toISOString()} to ${likeDate.toISOString()}`, + ); + await fs.utimes(filePath, likeDate, likeDate); + callbacks.onTimestampUpdate?.(track, fileDate, likeDate); + return { + track: track.title, + status: { success: true, updated: true }, + }; + } + return { + track: track.title, + status: { success: true, updated: false }, + }; + } catch (error) { + logger.error( + `Failed to verify/update timestamp for "${path.basename(filePath)}": ${ + error instanceof Error ? error.message : String(error) + }`, + ); + return { + track: track.title, + status: { + success: false, + updated: false, + error: error instanceof Error ? error.message : String(error), + }, + }; + } +}; + +export default async function verifyTimestamps( + likes: UserLike[], + folder: string, + callbacks: Callbacks = {}, +): Promise { + const availableMusic = (await fs.readdir(folder)).map(filename => path.parse(filename).name); + + const existingTracks = likes.filter(({ track }) => + availableMusic.includes(sanitiseFilename(track.title)), + ); + + return Promise.all( + existingTracks.map(async ({ track, created_at }) => { + const filePath = path.join(folder, `${sanitiseFilename(track.title)}.mp3`); + return verifyAndUpdateTimestamp(filePath, created_at, track, callbacks); + }), + ); +} diff --git a/src/types.ts b/src/types.ts index 17c3ff6..71a5e57 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,3 +98,20 @@ export interface DownloadResult { error?: string; }; } + +/** + * Result of a timestamp verification operation. + */ +export interface VerifyTimestampResult { + /** Title of the verified track */ + track: string; + /** Verification status */ + status: { + /** Whether the timestamp was verified successfully */ + success: boolean; + /** Whether the timestamp needed updating */ + updated: boolean; + /** Any error message if verification failed */ + error?: string; + }; +}