From 2825a360ffe9ea59d6fbad6fd8b68413731a9e45 Mon Sep 17 00:00:00 2001 From: Joshua Pohl Date: Sat, 13 Mar 2021 13:34:31 -0600 Subject: [PATCH] feat: add --add-mp3-metadata option (#21) --- README.md | 37 +++++++++++++++++++------------------ bin/bin.js | 19 +++++++++++++++++++ bin/util.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 133ef0b..31b0c2d 100644 --- a/README.md +++ b/README.md @@ -24,24 +24,25 @@ ## Options -| Option | Type | Required | Description | -| ----------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| --url | String | true | URL to podcast RSS feed. | -| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Templating" for more details. | -| --archive | String | false | Download or write out items not listed in archive file. Generates archive file at path if not found. See "Templating" for more details. | -| --episode-template | String | false | Template for generating episode related filenames. See "Templating" for details. | -| --include-meta | | false | Write out podcast metadata to JSON. | -| --include-episode-meta | | false | Write out individual episode metadata to JSON. | -| --ignore-episode-images | | false | Ignore downloading found images from --include-episode-meta. | -| --offset | Number | false | Offset starting download position. Default is 0. | -| --limit | Number | false | Max number of episodes to download. Downloads all by default. | -| --episode-regex | String | false | Match episode title against provided regex before starting download. | -| --override | | false | Override local files on collision. | -| --reverse | | false | Reverse download direction and start at last RSS item. | -| --info | | false | Print retrieved podcast info instead of downloading. | -| --list | | false | Print episode list instead of downloading. | -| --version | | false | Output the version number. | -| --help | | false | Output usage information. | +| Option | Type | Required | Description | +| ----------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| --url | String | true | URL to podcast RSS feed. | +| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Templating" for more details. | +| --archive | String | false | Download or write out items not listed in archive file. Generates archive file at path if not found. See "Templating" for more details. | +| --episode-template | String | false | Template for generating episode related filenames. See "Templating" for details. | +| --include-meta | | false | Write out podcast metadata to JSON. | +| --include-episode-meta | | false | Write out individual episode metadata to JSON. | +| --ignore-episode-images | | false | Ignore downloading found images from --include-episode-meta. | +| --offset | Number | false | Offset starting download position. Default is 0. | +| --limit | Number | false | Max number of episodes to download. Downloads all by default. | +| --episode-regex | String | false | Match episode title against provided regex before starting download. | +| --add-mp3-metadata | | false | Attempts to add a base level of MP3 metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) | +| --override | | false | Override local files on collision. | +| --reverse | | false | Reverse download direction and start at last RSS item. | +| --info | | false | Print retrieved podcast info instead of downloading. | +| --list | | false | Print episode list instead of downloading. | +| --version | | false | Output the version number. | +| --help | | false | Output usage information. | ## Archive diff --git a/bin/bin.js b/bin/bin.js index af21086..d41ea49 100644 --- a/bin/bin.js +++ b/bin/bin.js @@ -19,6 +19,7 @@ let { logItemsList, writeFeedMeta, writeItemMeta, + addMp3Metadata, } = require("./util"); let { createParseNumber, @@ -71,6 +72,10 @@ commander "--episode-regex ", "match episode title against regex before downloading" ) + .option( + "--add-mp3-metadata", + "attempts to add a base level of metadata to .mp3 files using ffmpeg" + ) .option("--override", "override local files on collision") .option("--reverse", "download episodes in reverse order") .option("--info", "print retrieved podcast info instead of downloading") @@ -92,6 +97,7 @@ let { reverse, info, list, + addMp3Metadata: addMp3MetadataFlag, } = commander; let main = async () => { @@ -256,6 +262,19 @@ let main = async () => { logError("Unable to download episode", error); } + if (addMp3MetadataFlag) { + try { + addMp3Metadata({ + feed, + item, + itemIndex: i, + outputPath: outputPodcastPath, + }); + } catch (error) { + logError("Unable to add episode metadata", error); + } + } + if (includeEpisodeMeta) { if (!ignoreEpisodeImages) { let episodeImageUrl = getImageUrl(item); diff --git a/bin/util.js b/bin/util.js index 7b965f6..b72fa7f 100644 --- a/bin/util.js +++ b/bin/util.js @@ -5,6 +5,8 @@ let stream = require("stream"); let path = require("path"); let fs = require("fs"); let got = require("got"); +let dayjs = require("dayjs"); +let { execSync } = require("child_process"); let { logError, logErrorAndExit } = require("./validate"); @@ -350,6 +352,55 @@ let getFeed = async (url) => { return feed; }; +let addMp3Metadata = ({ feed, item, itemIndex, outputPath }) => { + if (!fs.existsSync(outputPath)) { + return; + } + + if (!outputPath.endsWith(".mp3")) { + console.log("Not an .mp3 file. Unable to add metadata."); + return; + } + + let album = feed.title || ""; + let title = item.title || ""; + let artist = + item.itunes && item.itunes.author ? item.itunes.author : item.author || ""; + let track = + item.itunes && item.itunes.episode + ? item.itunes.episode + : `${feed.items.length - itemIndex}`; + let date = item.pubDate + ? dayjs(new Date(item.pubDate)).format("YYYY-MM-DD") + : ""; + + let metaKeysToVales = { + album, + artist, + title, + track, + date, + album_artist: album, + }; + + let metadataString = Object.keys(metaKeysToVales) + .map((key) => + metaKeysToVales[key] + ? `-metadata ${key}="${metaKeysToVales[key].replace(/"/g, '\\"')}"` + : null + ) + .filter((segment) => !!segment) + .join(" "); + + let tmpMp3Path = `${outputPath}.tmp.mp3`; + execSync( + `ffmpeg -loglevel quiet -i "${outputPath}" -map_metadata 0 ${metadataString} -codec copy "${tmpMp3Path}"` + ); + + fs.unlinkSync(outputPath); + fs.renameSync(tmpMp3Path, outputPath); +}; + module.exports = { download, getArchiveKey, @@ -361,6 +412,7 @@ module.exports = { logFeedInfo, logItemInfo, logItemsList, + addMp3Metadata, writeFeedMeta, writeItemMeta, };