diff --git a/.DS_Store b/.DS_Store index c9db3f3..1cb8fe5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/index.js b/index.js index 0c76ade..6211bd8 100644 --- a/index.js +++ b/index.js @@ -182,20 +182,17 @@ const navigateSpotifyTracks = async (token, playlistId, download_path, track_siz if (!tracks) { throw new Error("No tracks found in the playlist."); } - + const choices = tracks .map((t) => { + return { name: `${t.track.name} - ${t.track.artists.map((a) => a.name).join(", ")}`, // Visible to user value: { - name: t.track.name, - name: t.track.name, - duration_ms: t.track.duration_ms, name: t.track.name, duration_ms: t.track.duration_ms, artist: t.track.artists.map((a) => a.name), album: t.track.album.name, - duration_ms: t.track.duration_ms, image: t.track.album.images?.[0]?.url || "", album_url: t.track.album.href, external_url: t.track.external_urls.spotify, @@ -204,6 +201,8 @@ const navigateSpotifyTracks = async (token, playlistId, download_path, track_siz track_number: t.track.track_number, release_date: t.track.album.release_date, type: t.track.type, + explicit: t.track.explicit, + isrc: t.track.external_ids.isrc }, }; }); @@ -214,7 +213,7 @@ const navigateSpotifyTracks = async (token, playlistId, download_path, track_siz return; } - searchAndDownloadYTTrack(t.artist[0], t.name, download_path, 1); + searchAndDownloadYTTrack(t, download_path, 1); })) } catch (error) { console.error(`Error: ${error.message}`); diff --git a/kanpilot.toml b/kanpilot.toml index 0e7faa4..439cda8 100644 --- a/kanpilot.toml +++ b/kanpilot.toml @@ -17,17 +17,6 @@ name = "To Do" priority = "" linkCommits = [ ] - [[processes.tasks]] - id = "v1lpyqddavsj7twc1m5swhnc" - title = "Get a public playlist" - description = "Returns a public playlist given a spotify playlist url" - tag = "backlog" - linkFiles = [ ] - dueDate = "" - checkList = [ ] - priority = "" - linkCommits = [ ] - [[processes.tasks]] id = "llv5r9dmvk8pz34ekwnfsup1" title = "Implement basic user info" @@ -55,31 +44,20 @@ id = "process2" name = "In Progress" [[processes.tasks]] - id = "lvpws0ingpg1i6akfsp3x69h" - title = "Modify mp3 metadata with spotify data" - description = "" - tag = "cli" - linkFiles = [ ] - dueDate = "" - checkList = [ ] - priority = "High" - linkCommits = [ ] - - [[processes.tasks]] - id = "wvxa5wvg9tf5szf9sgwhuiys" - title = "Allow Spotituby plz" - description = '
Agree
Tell puppeteer to look at this button and do something about it
Use this
<button data-testid="auth-accept" data-encore-id="buttonPrimary" class="Button-sc-qlcn5g-0 hVnPpH"><span class="ButtonInner-sc-14ud5tc-0 dOeYIb encore-bright-accent-set"><p data-encore-id="type" class="Type__TypeElement-sc-goli3j-0 bkjCej">Agree</p></span><span class="ButtonFocus-sc-2hq6ey-0 iMcXKe"></span></button>
' - tag = "bug" + id = "thkjnadh8qoxuhzcekbfol5q" + title = "implement a jester" + description = "I have no idea what to test lmao" + tag = "backlog" linkFiles = [ ] dueDate = "" checkList = [ ] - priority = "High" + priority = "" linkCommits = [ ] [[processes.tasks]] - id = "thkjnadh8qoxuhzcekbfol5q" - title = "implement a jester" - description = "I have no idea what to test lmao" + id = "v1lpyqddavsj7twc1m5swhnc" + title = "Get a public playlist" + description = "Returns a public playlist given a spotify playlist url" tag = "backlog" linkFiles = [ ] dueDate = "" @@ -134,3 +112,25 @@ name = "Done" checkList = [ ] priority = "Low" linkCommits = [ ] + + [[processes.tasks]] + id = "wvxa5wvg9tf5szf9sgwhuiys" + title = "Allow Spotituby plz" + description = 'Agree
Tell puppeteer to look at this button and do something about it
Use this
<button data-testid="auth-accept" data-encore-id="buttonPrimary" class="Button-sc-qlcn5g-0 hVnPpH"><span class="ButtonInner-sc-14ud5tc-0 dOeYIb encore-bright-accent-set"><p data-encore-id="type" class="Type__TypeElement-sc-goli3j-0 bkjCej">Agree</p></span><span class="ButtonFocus-sc-2hq6ey-0 iMcXKe"></span></button>
' + tag = "bug" + linkFiles = [ ] + dueDate = "" + checkList = [ ] + priority = "High" + linkCommits = [ ] + + [[processes.tasks]] + id = "lvpws0ingpg1i6akfsp3x69h" + title = "Modify mp3 metadata with spotify data" + description = "" + tag = "cli" + linkFiles = [ ] + dueDate = "" + checkList = [ ] + priority = "High" + linkCommits = [ ] diff --git a/package.json b/package.json index 6d46962..67c549e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "spotituby", - "version": "1.0.0", + "version": "1.0.1", "main": "index.js", "type": "module", "scripts": { "app": "node index.js" }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "repository": { "type": "git", "url": "git+https://github.com/FrenzyExists/spotituby.git" diff --git a/src/utils/index.js b/src/utils/index.js index e707a72..0e2e3ef 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -166,18 +166,15 @@ const fetchLikedTracks = async (accessToken, pageSize = 50) => { // Paginate if pageSize exceeds 50 while (offset < pageSize) { const limit = Math.min(maxPageSize, pageSize - offset); // Fetch only the required amount - const response = await axios.get( - "https://api.spotify.com/v1/me/tracks", - { - headers: { - Authorization: `Bearer ${accessToken}` - }, - params: { - limit, - offset - } + const response = await axios.get("https://api.spotify.com/v1/me/tracks", { + headers: { + Authorization: `Bearer ${accessToken}` + }, + params: { + limit, + offset } - ); + }); totalTracks = totalTracks.concat(response.data.items); offset += limit; @@ -196,34 +193,65 @@ const fetchLikedTracks = async (accessToken, pageSize = 50) => { }; +const getFilenameFromCommand = (metadataCommand, outputDir) => { + return new Promise((resolve, reject) => { + const titleProcess = exec(metadataCommand); + + let filename = ""; + titleProcess.stdout.on("data", (data) => { + filename = `${outputDir}/${data.trim()}`; // Construct the filename + }); + console.log(filename); + + titleProcess.on("exit", (code) => { + if (code === 0) { + resolve(filename); + } else { + reject(new Error("Failed to retrieve filename")); + } + }); + + titleProcess.on("error", (err) => { + reject(err); + }); + }); +}; + + /** * Searches for a YouTube track based on artist and title, and downloads it in MP3 format. * - * @param {string|null} artist - The artist's name. Used in search query if provided. - * @param {string|null} title - The track title. Used in search query if provided. + * @param {object|null} metadata - Metadata stuff * @param {string|null} outputDir - The directory where the downloaded file will be saved. * @param {number} resultsCount - The number of search results to consider. Defaults to 1. * @param {boolean} search - Flag to determine if a search should be performed. Defaults to true. * @param {string|null} url - Direct URL of the track, bypassing search if provided. */ const searchAndDownloadYTTrack = async ( - artist = null, - title = null, + metadata = null, outputDir = null, resultsCount = 1, search = true, url = null ) => { + + const filterQuery = + search && !url + ? `--reject-title "official video|music video"` + : ""; const searchQuery = search && !url - ? `ytsearch${resultsCount}:"${artist} - ${title}"` - : `ytsearch${resultsCount}:"${artist} - ${title}"`; + ? `ytsearch${resultsCount}:"${metadata.artist} - ${metadata.name}"` + : ""; + const urlQuery = url && !search ? `url:${url}` : ""; - // {"downloaded_bytes": 4111336, "total_bytes": 4111336, "filename": "./downloads/Voyage - Dynamic.webm", "status": "finished", "elapsed": 0.9414091110229492, "ctx_id": null, "speed": 4367215.0097767385, "_speed_str": "4.16MiB/s", "_total_bytes_str": " 3.92MiB", "_elapsed_str": "00:00:00", "_percent_str": "100.0%", "_default_template": "100% of 3.92MiB in 00:00:00 at 4.16MiB/s"} + const downloadQuery = `-x --audio-format mp3 -o "${outputDir}/%(title)s.%(ext)s" --quiet --progress --progress-template "%(progress._percent_str)s - %(progress._total_bytes_str)s ETA %(progress._eta_str)s"`; - const command = `yt-dlp ${urlQuery} ${downloadQuery} ${searchQuery}`; + const command = `yt-dlp ${urlQuery} ${downloadQuery} ${searchQuery} ${filterQuery}`; + const metadataCommand = `yt-dlp ${urlQuery} ${searchQuery} ${filterQuery} --print "%(title)s.%(ext)s"`; + const process = exec(command); process.stdio[1].on("data", data => { console.log(data.toString()); @@ -236,9 +264,19 @@ const searchAndDownloadYTTrack = async ( process.kill(); }); - process.on("exit", () => { + process.on("exit", async () => { console.log("------------------------------------------"); console.log(`Saved at ${outputDir}`); + + try { + const filename = await getFilenameFromCommand(metadataCommand, outputDir); + console.log(`Title: ${filename}`); + + await writeMetadata(metadata, filename); + console.log("Metadata written successfully!"); + } catch (error) { + console.error("Error getting filename or writing metadata:", error); + } }); }; @@ -271,7 +309,7 @@ const fetchPlaylistTracks = async (accessToken, playlistId, pageSize = 50) => { break; } } - + return totalTracks; } catch (error) { console.error("Error fetching playlist tracks:", error.message); @@ -326,8 +364,64 @@ const trackSelector = async choices => { return selectedTracks; }; -const writeMetadata = (info, outputDir) => { - console.log("Havent implemented this sry 😂"); +const fetchImage = async imageUrl => { + try { + const response = await axios.get(imageUrl, { + responseType: "arraybuffer" + }); + + // Return the buffer and MIME type + return Buffer.from(response.data); + } catch (error) { + console.error("Error fetching image:", error.message); + throw new Error("Failed to fetch image"); + } +}; + + +/** + * Writes metadata tags to the specified music file. + * + * This function takes track information and a file path, + * then writes metadata tags such as title, artist, album, + * year, track number, and more to the music file. + * + * @param {object} info - An object containing metadata information about the track. + * @param {string} info.name - The title of the track. + * @param {string[]} info.artist - An array of artist names. + * @param {string} info.album - The album name of the track. + * @param {string} info.release_date - The release date of the track in YYYY-MM-DD format. + * @param {number} info.track_number - The track number in the album. + * @param {number} info.duration_ms - The duration of the track in milliseconds. + * @param {string} info.isrc - The International Standard Recording Code of the track. + * @param {string} info.image - URL to the image associated with the track. + * @param {boolean} info.explicit - Indicates if the track contains explicit content. + * @param {string} filepath - The path to the music file where metadata will be written. + */ +const writeMetadata = async (info, filepath) => { + // fetch image url + const img = await fetchImage(info.image); + + const tags = { + title: info.name, + artist: info.artist.join(", "), // TPE1 + album: info.album, // TALB + year: info.release_date.split("-")[0], // TYER + date: info.release_date.replace(/-/g, ""), // TDAT (Format: DDMM) + trackNumber: `${info.track_number}`, // TRCK + disc_number: info.disc_number, + length: `${Math.round(info.duration_ms / 1000)}`, // TLEN in seconds + ISRC: info.isrc, // TSRC + mediaType: "Digital", // TMED + APIC: img, + comment: info.explicit ? "Explicit content" : "Clean content", // COMM + originalTitle: info.name // TOAL + }; + + let f = filepath.replace(/\.[^/.]+$/, ".mp3"); + console.log(f); + + NodeID3.update(tags, f, (e, buff) => {}); }; const TOKENFILE = ".token";