{"id":15503845,"url":"https://github.com/kentcdodds/podcastify-dir","last_synced_at":"2025-04-22T23:52:38.654Z","repository":{"id":57124400,"uuid":"277712669","full_name":"kentcdodds/podcastify-dir","owner":"kentcdodds","description":"Take a directory of audio files and syndicate them with an rss feed","archived":false,"fork":false,"pushed_at":"2023-04-26T02:33:41.000Z","size":2158,"stargazers_count":46,"open_issues_count":2,"forks_count":10,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-29T20:11:17.709Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kentcdodds.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-07-07T04:03:07.000Z","updated_at":"2024-10-26T14:07:29.000Z","dependencies_parsed_at":"2025-03-04T17:34:32.293Z","dependency_job_id":"16d3b734-ceae-47a0-9078-838e882eb95a","html_url":"https://github.com/kentcdodds/podcastify-dir","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kentcdodds%2Fpodcastify-dir","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kentcdodds%2Fpodcastify-dir/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kentcdodds%2Fpodcastify-dir/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kentcdodds%2Fpodcastify-dir/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kentcdodds","download_url":"https://codeload.github.com/kentcdodds/podcastify-dir/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250343912,"owners_count":21415037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-02T09:14:54.794Z","updated_at":"2025-04-22T23:52:38.634Z","avatar_url":"https://github.com/kentcdodds.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003ch1\u003e@kentcdodds/podcastify-dir\u003c/h1\u003e\n\n\u003cp\u003eTake a directory of audio files and syndicate them with an rss feed\u003c/p\u003e\n\u003c/div\u003e\n\n---\n\n\u003c!-- prettier-ignore-start --\u003e\n[![Build Status][build-badge]][build]\n[![Code Coverage][coverage-badge]][coverage]\n[![version][version-badge]][package]\n[![downloads][downloads-badge]][npmtrends]\n[![MIT License][license-badge]][license]\n\n[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)\n[![PRs Welcome][prs-badge]][prs]\n[![Code of Conduct][coc-badge]][coc]\n\u003c!-- prettier-ignore-end --\u003e\n\n## The problem\n\nYou have a directory of audio files that you'd like to turn into a self-hosted\npodcast RSS feed.\n\nThere are various reasons you might want to do this. My use-case is I have 150\naudiobooks that I've purchased and I don't want to use Audible or Libro.fm (for\nexample) to listen to them so instead I've downloaded the MP3 files. It's too\nmuch to store on my device all at once, and I want to be able to cast them to my\nTV/speakers with Chromecast. I also need this to be password protected to avoid\nstealing my library.\n\n## This solution\n\nThis is a node server which will serve an RSS feed of all the audio files in a\ndirectory of your choosing. It uses express to handle GET requests to\n`/audiobook/feed.xml`, `/audiobook/:bookId/image`, and\n`audiobook/:bookId/audio.mp3` (the only one you need to ever access directly is\nthe feed).\n\nTo solve my specific problem, I store all my audiobooks on the Synology\nDiskStation (NAS) I have and run this server directly on that NAS (scaling is\nnot an issue because my family are the only ones that use it). From there, I use\na podcast app (the best I've found is PocketCasts) to consume the feed and\naccess/download all my audiobooks. Most podcast apps support reading the chapter\nID3 tags embedded in the audiobook files so you even get chapter support. It's\npretty seamless!\n\n## Table of Contents\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Project setup](#project-setup)\n- [Other exports](#other-exports)\n- [Other info](#other-info)\n  - [query string](#query-string)\n  - [cache](#cache)\n  - [Editing book metadata](#editing-book-metadata)\n- [Other Solutions](#other-solutions)\n- [About `@kentcdodds/` scoped and `kcd-` prefixed packages](#about-kentcdodds-scoped-and-kcd--prefixed-packages)\n- [Issues](#issues)\n  - [🐛 Bugs](#-bugs)\n  - [💡 Feature Requests](#-feature-requests)\n- [Contributors ✨](#contributors-)\n- [LICENSE](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Installation\n\nThis module is distributed via [npm][npm] which is bundled with [node][node] and\nshould be installed as one of your project's `dependencies`:\n\n```\nnpm install --save @kentcdodds/podcastify-dir\n```\n\n## Usage\n\n```javascript\nconst path = require('path')\nconst {startServer} = require('@kentcdodds/podcastify-dir')\n\nstartServer({\n  // the title will appear in the podcast app identifying this feed\n  title: 'Podcast Title',\n\n  // the description will normally appear on the feed's screen in the podcast app\n  description: 'Some great audiobooks',\n\n  // This image will show up in the podcast app for this feed\n  image: {\n    url: 'https://www.example.com/some-image.png',\n    link: 'https://www.example.com',\n    height: 500,\n    width: 500,\n\n    // I'm not 100% certain what these are for...\n    title: 'Some title for the image',\n    description: 'Some description for the image',\n  },\n\n  // the port you want to bind to (if not specified, it chooses a random port)\n  port: process.env.PORT,\n\n  // the directory of audio files\n  directory: path.join(__dirname, '..', 'audiobooks'),\n\n  // the username and password that will allow you to access the feed\n  users: {bob: 'the_builder'},\n\n  // this allows you to pass your own express app so you can configure it\n  // however you like. It defaults to creating one itself\n  app: express(),\n\n  // this allows you to specify where you want the routes to be mounted to\n  // by default it's /audiobook, but you could change it to \"/\" or \"/podcast\"\n  // if you'd like.\n  mountpath: '/audiobook',\n\n  // a little inversion of control here to allow you to modify the JS object\n  // that's converted to XML. We're using the `xml-js` npm module so you'll\n  // want to make sure your modifications will work with that package's\n  // `convert.js2xml` method\n  modifyXmlJs(xmlJs) {\n    xmlJs.rss.channel['itunes:author'] = 'Your name'\n    xmlJs.rss.channel['itunes:summary'] = 'Some other stuff'\n    return xmlJs\n  },\n})\n```\n\n`startServer` returns a promise with the started server in case that's useful.\nIt also ensures that the server is shut down properly if the process exits.\n\nThe server also supports rate limiting to help avoid people brute-forcing the\nusername/password.\n\n### Project setup\n\nIn my project, I only need a few things to get this running: `package.json`,\n`index.js`, and `forever.config.json`\n\n**`package.json`**:\n\nThis lists the dependencies and the scripts for the project.\n\n```json\n{\n  \"private\": true,\n  \"name\": \"doddsfam-audiobooks\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"stop\": \"FOREVER_ROOT=./.forever forever stopall\",\n    \"start\": \"FOREVER_ROOT=./.forever forever start ./forever.config.json\"\n  },\n  \"license\": \"UNLICENSED\",\n  \"dependencies\": {\n    \"@kentcdodds/podcastify-dir\": \"^1.4.2\",\n    \"forever\": \"^3.0.0\"\n  }\n}\n```\n\n**`forever.config.json`**:\n\n[`forever`](https://npm.im/forever) is a module that will ensure that if the\nserver stops for any reason, it is automatically restarted. That way you don't\nhave to log into your server to restart it if it crashed. Here's how I configure\nit:\n\n```json\n{\n  \"append\": true,\n  \"script\": \"index.js\",\n  \"sourceDir\": \".\",\n  \"logFile\": \"./forever.log\",\n  \"outFile\": \"./out.log\",\n  \"errFile\": \"./error.log\"\n}\n```\n\n**`index.js`**:\n\n```javascript\nconst {startServer} = require('@kentcdodds/podcastify-dir')\n\nstartServer({\n  title: 'Dodds Family Audiobooks',\n  description: 'The audiobooks of the Dodds family',\n  image: {\n    url: 'https://www.dropbox.com/s/some-id/some-filename.jpg?raw=1',\n    title: 'Dodds Family Audiobooks',\n    link: 'https://kentcdodds.com',\n    height: 500,\n    width: 500,\n  },\n  port: 8879,\n  directory: '/volume1/audiobooks/files',\n  users: {bob: 'the_builder'},\n  modifyXmlJs(xmlJs) {\n    xmlJs.rss.channel['itunes:author'] = 'Kent C. Dodds'\n    xmlJs.rss.channel['itunes:summary'] = 'Dodds Family Audiobooks'\n    return xmlJs\n  },\n})\n```\n\nBecause my NAS is accessible via the world-wide-web, I can use this URL in my\npodcast app:\n\n```\nhttp://bob:the_builder@example.com:8879/audiobook/feed.xml\n```\n\nThat's:\n\n```\nhttp://[username]:[password]@[domain_or_global_static_ip_address]:[port]/audiobook/feed.xml\n```\n\nI paste that into\n[PocketCasts's submit page](https://www.pocketcasts.com/submit) (specifying\n\"private\" so it's not indexed) to get a pocketcast URL for the podcast and then\nload that up in my pocketcasts account. I've had some success with other apps,\nbut I've had some trouble with all of them. I recommend experimenting a bit.\n\n## Other exports\n\n`getPodcastMiddleware` and `getPodcastRoutes` are also exported if you'd like to\nuse those more directly. I don't plan to document those, but feel free to\nexplore the source code if you need something more advanced.\n\n## Other info\n\n### query string\n\nThe `/audiobook/feed.xml` endpoint allows you to filter and sort the audiobooks\nvia the query string. You can also specify a custom image. This allows you to\nset up custom feeds for different categories of books. Here's a full example\n(put on multiple lines to simplify reading it):\n\n```\nhttp://bob:the_builder@example.com:8879/audiobook/feed.xml\n?filterIn=fantasy:category\n\u0026filterOut=poppins:title,joe:author\n\u0026sort=desc:pubDate,asc:duration\n\u0026title=Fantasy%20books\n\u0026image.url=https%3A%2F%2Fwww.dropbox.com%2Fs%2Fsome-id%2Fsome-name.png%3Fraw%3D1\n\u0026image.title=epic%20fantasy\n\u0026image.link=https%3A%2F%2Fkentcdodds.com\n\u0026image.height=500\n\u0026image.width=740\n\u0026image.description=epic%20fantasy\n```\n\nSo you have `title`, `filterIn`, `filterOut`, `sort`, and `image.*` options.\n\nThe fitler query params are a list of comma-separated filter sets which is a\npair of `[regex]:[property]`. The `sort` is `[direction]:[property]`.\n\nAdditionally, the `filterIn` and `filterOut` options can be used multiple times.\nSo if you want to filter in \"self help\" and \"career\" categories, then you could\nuse multiple `filterIn` params. For both `filterIn` and `filterOut`, the item\nmust match _all_ of the query options to apply.\n\nAlternatively, you could create individual feeds by starting multiple servers on\ndifferent ports and putting the audio files in different directories.\n\n### cache\n\nBecause reading the audiobook MP3 files for metadata can take some time (added\n300ms to the request when testing on my MacBook with just a few books), we cache\nthe metadata in memory. This is a pretty significant perf savings. However, if\nyou add a new book, or change metadata about a book, you'll want to delete the\ncache, so there's also a `/audiobook/bust-cache` endpoint you can hit with a GET\nrequest and it'll reset the cache.\n\n### Editing book metadata\n\nI use [Kid3](https://kid3.kde.org/) for editing book metadata. It works pretty\nwell. If you're audiobooks come from a reputable source (most of my books come\nfrom Audible which I download using [OpenAudible](https://openaudible.org/)),\nall the metadata should be set properly already. If you need to edit things\nmanually, here are the values you need to have set:\n\n- `title` - book title\n- `comment` - book summary\n- `asin` - book ID\n- `artist` - book author\n- `duration` - the time duration of the audiobook\n- `narrated_by` - the person (or people) who narrated the audiobook (optional)\n- `book_genre` or `genre` - A colon-separated list of applicable categories:\n  `Kids 8-10:Adventure:Fantasy`\n- `year` - The release date of the audiobook: `2020-01-23`\n- `APIC` - The Cover art (Kid3 allows you to drag-and-drop an image).\n\n## Other Solutions\n\nI'm not aware of any, if you are please [make a pull request][prs] and add it\nhere!\n\n## About `@kentcdodds/` scoped and `kcd-` prefixed packages\n\nIf a package I maintain is scoped to my username (`@kentcdodds`) or prefixed\nwith `kcd-`, that means I built and maintain it for myself. You're more than\nwelcome to use it, but I'm not likely to put much work into making it work for\nother people's use cases (I'm not heartless, I just don't have the time). If you\nhave a grander vision for the project, please feel free to bring it up in the\ncomments and perhaps we can collaborate on that vision and make it more\ngeneral-purpose (and remove the scope/prefix), but it's possible I'll recommend\nyou just fork the project and publish your own version.\n\n## Issues\n\n_Looking to contribute? Look for the [Good First Issue][good-first-issue]\nlabel._\n\n### 🐛 Bugs\n\nPlease file an issue for bugs, missing documentation, or unexpected behavior.\n\n[**See Bugs**][bugs]\n\n### 💡 Feature Requests\n\nPlease file an issue to suggest new features. Vote on feature requests by adding\na 👍. This helps maintainers prioritize what to work on.\n\n[**See Feature Requests**][requests]\n\n## Contributors ✨\n\nThanks goes to these people ([emoji key][emojis]):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://kentcdodds.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/1500684?v=3\" width=\"100px;\" alt=\"Kent C. Dodds\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eKent C. Dodds\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/kentcdodds/podcastify-dir/commits?author=kentcdodds\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/kentcdodds/podcastify-dir/commits?author=kentcdodds\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#infra-kentcdodds\" title=\"Infrastructure (Hosting, Build-Tools, etc)\"\u003e🚇\u003c/a\u003e \u003ca href=\"https://github.com/kentcdodds/podcastify-dir/commits?author=kentcdodds\" title=\"Tests\"\u003e⚠️\u003c/a\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-enable --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors][all-contributors] specification.\nContributions of any kind welcome!\n\n## LICENSE\n\nMIT\n\n\u003c!-- prettier-ignore-start --\u003e\n[npm]: https://www.npmjs.com\n[node]: https://nodejs.org\n[build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/podcastify-dir/validate?logo=github\u0026style=flat-square\n[build]: https://github.com/kentcdodds/podcastify-dir/actions?query=workflow%3Avalidate\n[coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/podcastify-dir.svg?style=flat-square\n[coverage]: https://codecov.io/github/kentcdodds/podcastify-dir\n[version-badge]: https://img.shields.io/npm/v/@kentcdodds/podcastify-dir.svg?style=flat-square\n[package]: https://www.npmjs.com/package/@kentcdodds/podcastify-dir\n[downloads-badge]: https://img.shields.io/npm/dm/@kentcdodds/podcastify-dir.svg?style=flat-square\n[npmtrends]: http://www.npmtrends.com/@kentcdodds/podcastify-dir\n[license-badge]: https://img.shields.io/npm/l/@kentcdodds/podcastify-dir.svg?style=flat-square\n[license]: https://github.com/kentcdodds/podcastify-dir/blob/master/LICENSE\n[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square\n[prs]: http://makeapullrequest.com\n[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square\n[coc]: https://github.com/kentcdodds/podcastify-dir/blob/master/other/CODE_OF_CONDUCT.md\n[emojis]: https://github.com/all-contributors/all-contributors#emoji-key\n[all-contributors]: https://github.com/all-contributors/all-contributors\n[bugs]: https://github.com/kentcdodds/podcastify-dir/issues?utf8=%E2%9C%93\u0026q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug\n[requests]: https://github.com/kentcdodds/podcastify-dir/issues?utf8=%E2%9C%93\u0026q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement\n[good-first-issue]: https://github.com/kentcdodds/podcastify-dir/issues?utf8=%E2%9C%93\u0026q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22\n\u003c!-- prettier-ignore-end --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkentcdodds%2Fpodcastify-dir","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkentcdodds%2Fpodcastify-dir","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkentcdodds%2Fpodcastify-dir/lists"}