Skip to content

Commit

Permalink
feat: support range params for fetching library (#68)
Browse files Browse the repository at this point in the history
* wip: parametrize library fetcher

* handle pagination manually

* return early when conditions are met

* add changesets

* bump tinyparse

* update docs

* improve error handling logic

* add core docs for data callback

* fix spelling in core docs

* update changeset

* add basic cli tests

* improve tests

* improve test coverage
  • Loading branch information
eegli authored Dec 26, 2022
1 parent 669f512 commit 87dbccd
Show file tree
Hide file tree
Showing 22 changed files with 736 additions and 466 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-terms-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotifly/library': minor
---

Customize the range when fetching the library. Either get the last `n` items or items after a specific date.
5 changes: 5 additions & 0 deletions .changeset/gentle-seahorses-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotifly/core': patch
---

Provide a `DataCallback` type to facilitate writing callbacks for curried convenience functions.
2 changes: 1 addition & 1 deletion packages/auth-token/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"cli"
],
"dependencies": {
"@eegli/tinyparse": "^0.7.0",
"@eegli/tinyparse": "^0.7.1",
"@spotifly/utils": "workspace:^",
"node-fetch": "2.6.7"
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function resolveOffsetPaginated<
let nextPage: string | null = null;
let offset = 0;
const responses = [];

do {
const response = (await getFn(arg1, {
...arg2,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function initialize(authOptions: AuthProviderOptions) {
}

export type SpotifyClient = ReturnType<typeof initialize>;
export type { DataPromise, DataResponse } from './types';
export type { DataCallback, DataPromise, DataResponse } from './types';
export type { AuthProviderOptions };
export { isError };

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export type LiteralUnion<T extends string> = T | (string & EmptyObject);

export type AnyFunc = (...args: any[]) => unknown;

export type DataCallback<
F extends (...args: any[]) => (...args: any[]) => unknown
> = Parameters<ReturnType<F>>[0];

// https://github.com/microsoft/TypeScript/issues/39556
export type BetterOmit<Type, Key> = {
[P in keyof Type as Exclude<P, Key>]: Type[P];
Expand Down
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"cli"
],
"dependencies": {
"@eegli/tinyparse": "^0.7.0",
"@eegli/tinyparse": "^0.7.1",
"@spotifly/core": "workspace:^",
"@spotifly/utils": "workspace:^",
"cli-progress": "^3.11.2"
Expand Down
21 changes: 21 additions & 0 deletions packages/library/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { createParser } from '@eegli/tinyparse';
import { Options } from './types';

const ZERO_DATE = new Date(0);

export const defaultConfig: Required<Options> = {
token: '',
type: 'light',
genres: false,
features: false,
compact: false,
outDir: '',
since: ZERO_DATE.toISOString(),
last: Infinity,
};

export const { parse, help } = createParser(defaultConfig, {
Expand Down Expand Up @@ -35,6 +39,23 @@ export const { parse, help } = createParser(defaultConfig, {
features: {
description: 'Include audio features for each track. Default: false',
},
since: {
description:
'Only include tracks added after this date. The date string must be formatted according to the ECMAScript Date Time String Format, e.g.: "YYYY-MM-DD". Default: All tracks',
customValidator: {
isValid(value) {
if (typeof value !== 'string') return false;
return !isNaN(new Date(value).getTime());
},
errorMessage(value) {
return `Invalid value '${value}' for option 'since'. Expected a valid date string`;
},
},
},
last: {
description:
'Only include the last n (most recent) tracks. Default: All tracks',
},
compact: {
description:
'Output more compact/minified JSON and save disk space. Default: false',
Expand Down
51 changes: 30 additions & 21 deletions packages/library/src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import * as Spotify from '@spotifly/core';
import { colors, writeJSON } from '@spotifly/utils';
import { initialize, isError } from '@spotifly/core';
import { writeJSON } from '@spotifly/utils';
import { defaultConfig } from './config';
import type { Library, LibraryHandler, TrackLight } from './types';
import { createProgressBar } from './utils';
import { createProgressBar, isBeforeDate } from './utils';

export const libraryHandler: LibraryHandler = async options => {
try {
const config = { ...defaultConfig, ...options };

const spotifyClient = Spotify.initialize({ accessToken: config.token });
const spotifyClient = initialize({ accessToken: config.token });

let library: Library = [];

let progress = createProgressBar('user library');

progress.start(0, 0);

await spotifyClient.Tracks.getAllUsersSavedTracks()(({ data }) => {
let nextPage: string | null = null;
let offset = 0;

fetchLoop: do {
const { data } = await spotifyClient.Tracks.getUsersSavedTracks({
limit: 50,
offset,
});
progress.setTotal(data.total);
progress.increment(data.items.length);

data.items.forEach(el => {
library.push(el);
});
});
for (let i = 0; i < data.items.length; i++) {
const track = data.items[i];
if (library.length === config.last) {
break fetchLoop;
}
if (isBeforeDate(track.added_at, config.since)) {
break fetchLoop;
}
library.push(track);
}
nextPage = data.next;
offset += 50;
} while (nextPage);

progress.stop();

Expand Down Expand Up @@ -110,17 +125,11 @@ export const libraryHandler: LibraryHandler = async options => {
console.info("Success! Library written to '%s'", outDir);
return libExport;
} catch (error) {
if (Spotify.isError(error)) {
console.error(
colors.red(
`\nError talking to Spotify:\n${JSON.stringify(
error.response?.data.error,
null,
2
)}`
)
);
if (isError(error)) {
const { status, message } = error.response!.data.error;
throw new Error(`Status ${status}, ${message}`);
} else {
throw new Error('Something went wrong');
}
throw error;
}
};
2 changes: 2 additions & 0 deletions packages/library/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type Options = {
features?: boolean;
compact?: boolean;
outDir?: string;
since?: string;
last?: number;
};

export type TrackLight = {
Expand Down
12 changes: 12 additions & 0 deletions packages/library/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@ export const createProgressBar = (items: string) =>
barIncompleteChar: ' ',
hideCursor: true,
});

/**
* Checks if the first date as a string is before another date
*/
export const isBeforeDate = (date1: string, date2: string): boolean => {
const d1 = Date.parse(date1);
const d2 = Date.parse(date2);
if (isNaN(d1) || isNaN(d2)) {
return false;
}
return d1 < d2;
};
Loading

0 comments on commit 87dbccd

Please sign in to comment.