From 7a5e8785d8514c29f4e74f03946db987f47f808b Mon Sep 17 00:00:00 2001 From: Ak5cel Date: Mon, 15 Jan 2024 20:26:13 +0530 Subject: [PATCH] fetch liked songs and metadata on init * extracts out the functions for fetching all liked songs and their metadata into a common file, so that it can be used in both `export` and `init` * updates README to remove the note asking users to run an export before `show-genres`. Liked songs and their metadata are fetched on init itself, so `show-genres` can be run after an init without needing an export first. --- README.md | 46 +++++++++------------ commands/common.js | 81 ++++++++++++++++++++++++++++++++++++ commands/export-tracks.js | 87 ++++----------------------------------- commands/init.js | 9 ++++ 4 files changed, 116 insertions(+), 107 deletions(-) create mode 100644 commands/common.js diff --git a/README.md b/README.md index 057135b..f044514 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # heartify-cli -A CLI tool to export and filter out your Spotify liked songs into playlists. +A CLI tool to export and filter out your Spotify liked songs into playlists. With support for filters like ranges of date added, release date, genres, audio features, and more. - ## What does it solve? Around 2500 Liked songs in, I needed a way to organise the chaos without having to move away from 'liking' any more songs. @@ -11,24 +10,23 @@ Hopefully, it's useful to more people out there with little time and a lot of li ### What you can do - ✔️ Export them to a playlist - you can now share your liked songs! - - ✔️ Filter by the year you liked them - make a 'My Top Songs 2023' playlist, for example - - ✔️ Filter by genre(s) to make genre-mixes out of your liked songs - - ✔️ Make monthly playlists - no more adding songs manually to monthly playlists, simply filter by ranges of dates added - - ✔️ Decade mixes - filter by release date - - ✔️ Filter by audio features - make a workout playlist of songs in a certain bpm range, for example +✔️ Export them to a playlist - you can now share your liked songs! + +✔️ Filter by the year you liked them - make a 'My Top Songs 2023' playlist, for example + +✔️ Filter by genre(s) to make genre-mixes out of your liked songs + +✔️ Make monthly playlists - no more adding songs manually to monthly playlists, simply filter by ranges of dates added + +✔️ Decade mixes - filter by release date + +✔️ Filter by audio features - make a workout playlist of songs in a certain bpm range, for example ### No library size limits There's no (known) limit to the number of songs Heartify can fetch, so bring along your massive library of 7000 liked songs (or more?)! Just be prepared for it to take a bit longer with the measures in place to account for Spotify's rate limits. - ## Getting Started ### Installation @@ -41,7 +39,7 @@ npm install -g heartify-cli ### Authorisation -Run the following command from any directory, all data is stored locally where Heartify is installed. +Run the following command from any directory, all data is stored locally where Heartify is installed. Then follow the instructions to authorise access to your Spotify library. ```sh @@ -49,8 +47,8 @@ Then follow the instructions to authorise access to your Spotify library. ``` -This command needs to be run just once, and you're logged in until you revoke permissions from your account page, -logout, or until Spotify automatically revokes permissions from time-to-time (in which case, run `heartify init` again). +This command needs to be run just once, and you're logged in until you revoke permissions from your account page, +logout, or until Spotify automatically revokes permissions from time-to-time (in which case, run `heartify init` again). Heartify uses the OAuth 2.0 Authorization Code with PKCE flow, and refreshes the access token automatically until it's revoked. ### Basic Examples @@ -85,11 +83,6 @@ Heartify uses the OAuth 2.0 Authorization Code with PKCE flow, and refreshes the ``` - > Note: - > As of now, you need to have run at least one `export` command before running `show-genres` as it needs to fetch the - > liked songs first while exporting to identify the genres. This will be fixed in a future update. - - Then pick a genre and filter (replace '\'): ```sh @@ -160,12 +153,11 @@ Heartify uses the OAuth 2.0 Authorization Code with PKCE flow, and refreshes the ``` - ## Detailed Docs (TODO) ### `--filter` -Filters are of the form `field=value` and accept any field from the list of supported fields below. +Filters are of the form `field=value` and accept any field from the list of supported fields below. The value can be either individual values like in `genre=disco` and `time_signature=4`, or ranges like in `tempo=[100,120]` (only DateTime and Number fields accept ranges at the time of writing) @@ -209,7 +201,7 @@ Filters for different fields are joined by AND ``` -Filters do not need to be wrapped in quotes as long as they do not contain ANY spaces. +Filters do not need to be wrapped in quotes as long as they do not contain ANY spaces. Wrap them in single/double quotes when they contain whitespace. ```sh @@ -231,7 +223,7 @@ Wrap them in single/double quotes when they contain whitespace. ``` ### String fields - + - `artist` - `genre` @@ -249,7 +241,7 @@ These fields correspond to those returned by the Spotify Web API. The descriptio which can be found in the [Spotify Web Api docs for Audio Features](https://developer.spotify.com/documentation/web-api/reference/get-audio-features). | field | description | range | -|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------| +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | | `danceability` | How suitable a track is for dancing based on
a combination of musical elements including tempo,
rhythm stability, beat strength,and overall regularity | 0.0 to 1.0
(float) | | `energy` | Represents a perceptual measure of intensity
and activity | 0.0 to 1.0
(float) | | `key` | The key the track is in.
Integers map to pitches using standard Pitch Class
notation. E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on.
If no key was detected, the value is -1. | -1 to 11
(integer) | diff --git a/commands/common.js b/commands/common.js new file mode 100644 index 0000000..2256f4b --- /dev/null +++ b/commands/common.js @@ -0,0 +1,81 @@ +const { + fetchLikedSongs, + fetchArtists, + fetchAudioFeatures, +} = require("../libs/api"); +const { + saveTrack, + getFetchedArtists, + batchSaveGenres, + getFetchedTracks, + batchSaveAudioFeatures, +} = require("../libs/db"); +const pc = require("picocolors"); + +exports.fetchAllLikedSongs = async () => { + const header = "Fetching Liked songs..."; + process.stdout.write(header); + + let count = 0; + for await (let [savedTrackObj, total] of fetchLikedSongs()) { + saveTrack(savedTrackObj); + + count++; + + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + process.stdout.write(pc.dim(`[${count}/${total}]`)); + } + + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + console.log(pc.green("Done.")); +}; + +exports.fetchGenres = async () => { + const header = "Fetching genre data..."; + process.stdout.write(header); + + let count = 0; + for await (let artistIDs of getFetchedArtists()) { + const artists = await fetchArtists(artistIDs); + const genreData = artists.map((artist) => { + return { artistID: artist.id, genres: artist.genres }; + }); + + batchSaveGenres(genreData); + + count += artistIDs.length; + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + process.stdout.write(pc.dim(`[${count}] artists`)); + } + + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + process.stdout.write(pc.green(`Done.\n`)); +}; + +exports.fetchAllAudioFeatures = async () => { + const header = "Fetching audio features..."; + process.stdout.write(header); + + let count = 0; + for await (let trackIDs of getFetchedTracks({}, 100)) { + const audioFeatures = await fetchAudioFeatures(trackIDs); + const trackFeatures = audioFeatures.map((featuresObj) => { + return { trackID: featuresObj.id, audioFeatures: featuresObj }; + }); + + batchSaveAudioFeatures(trackFeatures); + + count += trackIDs.length; + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + process.stdout.write(pc.dim(`[${count}] songs`)); + } + + process.stdout.cursorTo(header.length); + process.stdout.clearLine(+1); + process.stdout.write(pc.green(`Done.\n`)); +}; diff --git a/commands/export-tracks.js b/commands/export-tracks.js index a8d9578..a9bf857 100644 --- a/commands/export-tracks.js +++ b/commands/export-tracks.js @@ -1,20 +1,15 @@ +const { createPlaylist, addTracksToPlaylist } = require("../libs/api"); const { - fetchLikedSongs, - createPlaylist, - addTracksToPlaylist, - fetchArtists, - fetchAudioFeatures, -} = require("../libs/api"); -const { - saveTrack, clearRecords, checkIsDBUpToDate, getFetchedTracks, - batchSaveGenres, - getFetchedArtists, - batchSaveAudioFeatures, checkTablesExist, } = require("../libs/db"); +const { + fetchAllLikedSongs, + fetchGenres, + fetchAllAudioFeatures, +} = require("./common"); const pc = require("picocolors"); exports.exportTracks = async (playlistName, options) => { @@ -32,7 +27,7 @@ exports.exportTracks = async (playlistName, options) => { } else { console.log("Changes detected. Updating data.\n"); clearRecords(); - await reFetchLikedSongs(); + await fetchAllLikedSongs(); await fetchGenres(); await fetchAllAudioFeatures(); } @@ -80,71 +75,3 @@ exports.exportTracks = async (playlistName, options) => { console.log(`\n\n\t${playlistURI}\n\n`); console.log("in the Spotify desktop client."); }; - -const reFetchLikedSongs = async () => { - const header = "Fetching Liked songs..."; - process.stdout.write(header); - - let count = 0; - for await (let [savedTrackObj, total] of fetchLikedSongs()) { - saveTrack(savedTrackObj); - - count++; - - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - process.stdout.write(pc.dim(`[${count}/${total}]`)); - } - - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - console.log(pc.green("Done.")); -}; - -const fetchGenres = async () => { - const header = "Fetching genre data..."; - process.stdout.write(header); - - let count = 0; - for await (let artistIDs of getFetchedArtists()) { - const artists = await fetchArtists(artistIDs); - const genreData = artists.map((artist) => { - return { artistID: artist.id, genres: artist.genres }; - }); - - batchSaveGenres(genreData); - - count += artistIDs.length; - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - process.stdout.write(pc.dim(`[${count}] artists`)); - } - - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - process.stdout.write(pc.green(`Done.\n`)); -}; - -const fetchAllAudioFeatures = async () => { - const header = "Fetching audio features..."; - process.stdout.write(header); - - let count = 0; - for await (let trackIDs of getFetchedTracks({}, 100)) { - const audioFeatures = await fetchAudioFeatures(trackIDs); - const trackFeatures = audioFeatures.map((featuresObj) => { - return { trackID: featuresObj.id, audioFeatures: featuresObj }; - }); - - batchSaveAudioFeatures(trackFeatures); - - count += trackIDs.length; - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - process.stdout.write(pc.dim(`[${count}] songs`)); - } - - process.stdout.cursorTo(header.length); - process.stdout.clearLine(+1); - process.stdout.write(pc.green(`Done.\n`)); -}; diff --git a/commands/init.js b/commands/init.js index ec421ef..1b4d2c0 100644 --- a/commands/init.js +++ b/commands/init.js @@ -12,6 +12,11 @@ const { createUserWithTokens, dropAllTables, } = require("../libs/db"); +const { + fetchAllLikedSongs, + fetchGenres, + fetchAllAudioFeatures, +} = require("./common"); exports.init = async () => { const version = require("../package.json").version; @@ -73,6 +78,10 @@ exports.init = async () => { console.log(pc.green("Done.\n")); + await fetchAllLikedSongs(); + await fetchGenres(); + await fetchAllAudioFeatures(); + console.log(pc.green("Init complete.\n")); console.log( `Run \`heartify export --help\` to see how to make your first playlist!`