diff --git a/README.md b/README.md index 9a03b3e..b7067e3 100644 --- a/README.md +++ b/README.md @@ -24,40 +24,41 @@ Either `--url` or `--file` must be provided. Type values surrounded in square brackets (`[]`) can be used as used as boolean options (no argument required). -| Option | Type | Required | Description | -| ----------------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| --url | String | true\* | URL to podcast RSS feed. | -| --file | String | true\* | Path to local RSS file. | -| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. | -| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. | -| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. | -| --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. | -| --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. | -| --include-meta | | false | Write out podcast metadata to JSON. | -| --include-episode-meta | | false | Write out individual episode metadata **to** JSON. | -| --include-episode-images | | false | Download found episode images. | -| --include-episode-transcripts | | false | Download found episode transcripts. | -| --offset | Number | false | Offset starting download position. Default is 0. | -| --limit | Number | false | Max number of episodes to download. Downloads all by default. | -| --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). | -| --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) | -| --episode-regex | String | false | Match episode title against provided regex before starting download. | -| --episode-digits | Number | false | Minimum number of digits to use for episode numbering (e.g. 3 would generate "001" instead of "1"). Default is 0. | -| --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. | -| --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". | -| --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". | -| --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) | -| --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) | -| --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) | -| --override | | false | Override local files on collision. | -| --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. | -| --reverse | | false | Reverse download direction and start at last RSS item. | -| --info | | false | Print retrieved podcast info instead of downloading. | -| --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. | -| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. | -| --parser-config | String | false | Path to JSON file that will be parsed and used to override the default config passed to [rss-parser](https://github.com/rbren/rss-parser#xml-options). | -| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). | -| --help | | false | Output usage information. | +| Option | Type | Required | Description | +| --------------------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| --url | String | true\* | URL to podcast RSS feed. | +| --file | String | true\* | Path to local RSS file. | +| --out-dir | String | false | Specify output directory for episodes and metadata. Defaults to "./{{podcast_title}}". See "Template Options" for more details. | +| --threads | Number | false | Determines the number of downloads that will happen concurrently. Default is 1. | +| --attempts | Number | false | Sets the number of download attempts per individual file. Default is 3. | +| --archive | [String] | false | Download or write out items not listed in archive file. Generates archive file at path if not found. Defaults to "./{{podcast_title}}/archive.json" when used as a boolean option. See "Template Options" for more details. | +| --episode-template | String | false | Template for generating episode related filenames. See "Template Options" for details. | +| --episode-custom-template-options | | false | Provide custom options for the episode template. See "Template Options" for details. | +| --include-meta | | false | Write out podcast metadata to JSON. | +| --include-episode-meta | | false | Write out individual episode metadata **to** JSON. | +| --include-episode-images | | false | Download found episode images. | +| --include-episode-transcripts | | false | Download found episode transcripts. | +| --offset | Number | false | Offset starting download position. Default is 0. | +| --limit | Number | false | Max number of episodes to download. Downloads all by default. | +| --after | String | false | Only download episodes after this date (i.e. MM/DD/YYY, inclusive). | +| --before | String | false | Only download episodes before this date (i.e. MM/DD/YYY, inclusive) | +| --episode-regex | String | false | Match episode title against provided regex before starting download. | +| --episode-digits | Number | false | Minimum number of digits to use for episode numbering (e.g. 3 would generate "001" instead of "1"). Default is 0. | +| --episode-num-offset | Number | false | Offset the acquired episode number. Default is 0. | +| --episode-source-order | String | false | Attempted order to extract episode audio URL from RSS feed. Default is "enclosure,link". | +| --episode-transcript-types | String | false | List of allowed transcript types in preferred order. Default is "application/json,application/x-subrip,application/srr,application/srt,text/vtt,text/html,text/plain". | +| --add-mp3-metadata | | false | Attempts to add a base level of episode metadata to each episode. Recommended only in cases where the original metadata is of poor quality. (**ffmpeg required**) | +| --adjust-bitrate | String (e.g. "48k") | false | Attempts to adjust bitrate of episodes. (**ffmpeg required**) | +| --mono | | false | Attempts to force episodes into mono. (**ffmpeg required**) | +| --override | | false | Override local files on collision. | +| --always-postprocess | | false | Always run additional tasks on the file regardless if the file already exists. This includes --add-mp3-metadata, --adjust-bitrate, --mono, and --exec. | +| --reverse | | false | Reverse download direction and start at last RSS item. | +| --info | | false | Print retrieved podcast info instead of downloading. | +| --list | [String] | false | Print episode list instead of downloading. Defaults to "table" when used as a boolean option. "json" is also supported. | +| --exec | String | false | Execute a command after each episode is downloaded. See "Template Options" for more details. | +| --parser-config | String | false | Path to JSON file that will be parsed and used to override the default config passed to [rss-parser](https://github.com/rbren/rss-parser#xml-options). | +| --proxy | | false | Enable proxy support. Specify environment variables listed by [global-agent](https://github.com/gajus/global-agent#environment-variables). | +| --help | | false | Output usage information. | ## Archive @@ -88,6 +89,12 @@ Options that support templates allow users to specify a template for the generat - `podcast_link`: `link` value provided for the podcast feed. Typically the homepage URL. - `guid`: The GUID of the episode. +#### `--episode-custom-template-options` + +Each matcher provided will be used to extract a value from the episode `title`. Access these values in the template using the `custom_` keyword where `` is the index of the matcher provided (starting from `0`). + +If no match is found, the `custom_` keyword will be replaced with an empty string. + ### `--exec` - `episode_path`: The path to the downloaded episode. diff --git a/bin/async.js b/bin/async.js index dae9b80..93eff08 100644 --- a/bin/async.js +++ b/bin/async.js @@ -70,7 +70,7 @@ const download = async (options) => { }, }); } catch (error) { - // unable to retrive head response + // unable to retrieve head response } const tempOutputPath = getTempPath(outputPath); @@ -164,6 +164,7 @@ const downloadItemsAsync = async ({ basePath, bitrate, episodeTemplate, + episodeCustomTemplateOptions, episodeDigits, episodeNumOffset, episodeSourceOrder, @@ -199,6 +200,7 @@ const downloadItemsAsync = async ({ url: episodeAudioUrl, ext: audioFileExt, template: episodeTemplate, + customTemplateOptions: episodeCustomTemplateOptions, width: episodeDigits, offset: episodeNumOffset, }); @@ -283,6 +285,7 @@ const downloadItemsAsync = async ({ url: episodeAudioUrl, ext: episodeMetaExt, template: episodeTemplate, + customTemplateOptions: episodeCustomTemplateOptions, width: episodeDigits, offset: episodeNumOffset, }); diff --git a/bin/bin.js b/bin/bin.js index 0299c5a..9698a79 100755 --- a/bin/bin.js +++ b/bin/bin.js @@ -2,7 +2,7 @@ import fs from "fs"; import _path from "path"; -import commander from "commander"; +import { program } from "commander"; import pluralize from "pluralize"; import { bootstrap as bootstrapProxy } from "global-agent"; @@ -29,7 +29,7 @@ import { import { getFolderName, getSimpleFilename } from "./naming.js"; import { downloadItemsAsync } from "./async.js"; -setupCommander(commander, process.argv); +const opts = setupCommander(program); const { after, @@ -41,6 +41,7 @@ const { episodeRegex, episodeSourceOrder, episodeTemplate, + episodeCustomTemplateOptions, episodeTranscriptTypes, exec, file, @@ -62,9 +63,9 @@ const { url, addMp3Metadata: addMp3MetadataFlag, adjustBitrate: bitrate, -} = commander; +} = opts; -let { archive } = commander; +let { archive } = opts; const main = async () => { if (!url && !file) { @@ -213,6 +214,7 @@ const main = async () => { episodeRegex, episodeSourceOrder, episodeTemplate, + episodeCustomTemplateOptions, includeEpisodeImages, includeEpisodeTranscripts, episodeTranscriptTypes, @@ -234,6 +236,7 @@ const main = async () => { basePath, bitrate, episodeTemplate, + episodeCustomTemplateOptions, episodeDigits, episodeNumOffset, episodeSourceOrder, diff --git a/bin/commander.js b/bin/commander.js index bb2a574..3dd3b8e 100644 --- a/bin/commander.js +++ b/bin/commander.js @@ -6,8 +6,8 @@ import { import { createParseNumber, hasFfmpeg } from "./validate.js"; import { logErrorAndExit } from "./logger.js"; -export const setupCommander = (commander, argv) => { - commander +export const setupCommander = (program) => { + program .option("--url ", "url to podcast rss feed") .option("--file ", "local path to podcast rss feed") .option( @@ -24,6 +24,10 @@ export const setupCommander = (commander, argv) => { "template for generating episode related filenames", "{{release_date}}-{{title}}" ) + .option( + "--episode-custom-template-options ", + "create custom options for the episode template" + ) .option( "--episode-digits ", "minimum number of digits to use for episode numbering (leading zeros)", @@ -180,6 +184,9 @@ export const setupCommander = (commander, argv) => { "--parser-config ", "path to JSON config to override RSS parser" ) - .option("--proxy", "enable proxy support via global-agent") - .parse(argv); + .option("--proxy", "enable proxy support via global-agent"); + + program.parse(); + + return program.opts(); }; diff --git a/bin/naming.js b/bin/naming.js index 7611177..e4e046a 100644 --- a/bin/naming.js +++ b/bin/naming.js @@ -25,15 +25,24 @@ const getItemFilename = ({ feed, template, width, + customTemplateOptions = [], offset = 0, }) => { const episodeNum = feed.items.length - item._originalIndex + offset; + const title = item.title || ""; const formattedPubDate = item.pubDate ? dayjs(new Date(item.pubDate)).format("YYYYMMDD") : null; + const customReplacementTuples = customTemplateOptions.map((option, i) => { + const matchRegex = new RegExp(option); + const match = title.match(matchRegex); + + return match && match[0] ? [`custom_${i}`, match[0]] : [`custom_${i}`, ""]; + }); + const templateReplacementsTuples = [ - ["title", item.title || ""], + ["title", title], ["release_date", formattedPubDate || ""], ["episode_num", `${episodeNum}`.padStart(width, "0")], ["url", url], @@ -41,6 +50,7 @@ const getItemFilename = ({ ["podcast_link", feed.link || ""], ["duration", item.itunes?.duration || ""], ["guid", item.guid], + ...customReplacementTuples, ]; const templateSegments = template.trim().split(path.sep); diff --git a/bin/util.js b/bin/util.js index c876e6a..e1857a4 100644 --- a/bin/util.js +++ b/bin/util.js @@ -165,6 +165,7 @@ const getItemsToDownload = ({ episodeRegex, episodeSourceOrder, episodeTemplate, + episodeCustomTemplateOptions, includeEpisodeImages, includeEpisodeTranscripts, episodeTranscriptTypes, @@ -252,6 +253,7 @@ const getItemsToDownload = ({ url: episodeAudioUrl, ext: episodeImageFileExt, template: episodeTemplate, + customTemplateOptions: episodeCustomTemplateOptions, width: episodeDigits, offset: episodeNumOffset, }); diff --git a/build.cjs b/build.cjs index ea4e5d2..031bf1b 100644 --- a/build.cjs +++ b/build.cjs @@ -6,15 +6,15 @@ const { version } = require("./package.json"); const targetMap = [ { - target: "node14-linux-x64", + target: "node18-linux-x64", output: `./binaries/podcast-dl-${version}-linux-x64`, }, { - target: "node14-macos-x64", + target: "node18-macos-x64", output: `./binaries/podcast-dl-${version}-macos-x64`, }, { - target: "node14-win-x64", + target: "node18-win-x64", output: `./binaries/podcast-dl-${version}-win-x64`, }, ]; diff --git a/docs/examples.md b/docs/examples.md index 7e1ec4c..6bb2ddc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -53,3 +53,9 @@ npx podcast-dl --episode-regex "Zelda" --url "http://eightfour.libsyn.com/rss" ```bash npx podcast-dl --url "http://eightfour.libsyn.com/rss" --exec "ffmpeg -i {{episode_path}} -b:a 192k -f mp3 {{episode_path_base}}/{{episode_filename_base}}-192k.mp3" ``` + +## Extract "foo" and "bar" from the episode title and place it in the episode filename + +```bash +npx podcast-dl --url "http://eightfour.libsyn.com/rss" --episode-custom-template-options "foo" "bar" --episode-template "{{custom_0}}-{{custom_1}}-{{episode_num}}"" +``` diff --git a/package.json b/package.json index e939482..895168c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "cli" ], "engines": { - "node": ">=14.17.6" + "node": ">=18.17.0" }, "repository": { "type": "git", @@ -49,7 +49,7 @@ }, "dependencies": { "command-exists": "^1.2.9", - "commander": "^5.1.0", + "commander": "^12.1.0", "dayjs": "^1.8.25", "filenamify": "^6.0.0", "global-agent": "^3.0.0",