Skip to content

Commit

Permalink
separate verify timestamps from missing music
Browse files Browse the repository at this point in the history
  • Loading branch information
realies committed Jan 12, 2025
1 parent 4356b13 commit c490191
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 85 deletions.
60 changes: 48 additions & 12 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,22 @@ Downloads missing tracks from a list of liked tracks.
function getMissingMusic(
likes: UserLike[],
folder?: string,
callbacks?: Callbacks
callbacks?: Callbacks,
): Promise<DownloadResult[]>
```

#### 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<VerifyTimestampResult[]>
```

### Types

```typescript
Expand Down Expand Up @@ -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;
};
}
```
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
53 changes: 30 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
50 changes: 0 additions & 50 deletions src/services/getMissingMusic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -39,43 +38,10 @@ const downloadArtwork = async (url: string): Promise<ArrayBuffer | null> => {
}
};

const verifyAndUpdateTimestamp = async (
filePath: string,
created_at: string,
track: Track,
callbacks: Callbacks,
): Promise<boolean> => {
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<DownloadResult[]> {
try {
await fs.access(folder);
Expand All @@ -85,29 +51,13 @@ 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 }) =>
!availableMusic.includes(sanitiseFilename(track.title)) &&
track.media.transcodings.length > 0,
);

// Only return download results
return Promise.all(
missingTracks.map(async ({ track, created_at }) => {
callbacks.onDownloadStart?.(track);
Expand Down
68 changes: 68 additions & 0 deletions src/services/verifyTimestamps.ts
Original file line number Diff line number Diff line change
@@ -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<VerifyTimestampResult> => {
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<VerifyTimestampResult[]> {
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);
}),
);
}
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

0 comments on commit c490191

Please sign in to comment.