@kentcdodds/podcastify-dir
v1.7.1
Published
Take a directory of audio files and syndicate them with an rss feed
Downloads
20
Readme
The problem
You have a directory of audio files that you'd like to turn into a self-hosted podcast RSS feed.
There are various reasons you might want to do this. My use-case is I have 150 audiobooks that I've purchased and I don't want to use Audible or Libro.fm (for example) to listen to them so instead I've downloaded the MP3 files. It's too much to store on my device all at once, and I want to be able to cast them to my TV/speakers with Chromecast. I also need this to be password protected to avoid stealing my library.
This solution
This is a node server which will serve an RSS feed of all the audio files in a
directory of your choosing. It uses express to handle GET requests to
/audiobook/feed.xml
, /audiobook/:bookId/image
, and
audiobook/:bookId/audio.mp3
(the only one you need to ever access directly is
the feed).
To solve my specific problem, I store all my audiobooks on the Synology DiskStation (NAS) I have and run this server directly on that NAS (scaling is not an issue because my family are the only ones that use it). From there, I use a podcast app (the best I've found is PocketCasts) to consume the feed and access/download all my audiobooks. Most podcast apps support reading the chapter ID3 tags embedded in the audiobook files so you even get chapter support. It's pretty seamless!
Table of Contents
- Installation
- Usage
- Other exports
- Other info
- Other Solutions
- About
@kentcdodds/
scoped andkcd-
prefixed packages - Issues
- Contributors ✨
- LICENSE
Installation
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies
:
npm install --save @kentcdodds/podcastify-dir
Usage
const path = require('path')
const {startServer} = require('@kentcdodds/podcastify-dir')
startServer({
// the title will appear in the podcast app identifying this feed
title: 'Podcast Title',
// the description will normally appear on the feed's screen in the podcast app
description: 'Some great audiobooks',
// This image will show up in the podcast app for this feed
image: {
url: 'https://www.example.com/some-image.png',
link: 'https://www.example.com',
height: 500,
width: 500,
// I'm not 100% certain what these are for...
title: 'Some title for the image',
description: 'Some description for the image',
},
// the port you want to bind to (if not specified, it chooses a random port)
port: process.env.PORT,
// the directory of audio files
directory: path.join(__dirname, '..', 'audiobooks'),
// the username and password that will allow you to access the feed
users: {bob: 'the_builder'},
// this allows you to pass your own express app so you can configure it
// however you like. It defaults to creating one itself
app: express(),
// this allows you to specify where you want the routes to be mounted to
// by default it's /audiobook, but you could change it to "/" or "/podcast"
// if you'd like.
mountpath: '/audiobook',
// a little inversion of control here to allow you to modify the JS object
// that's converted to XML. We're using the `xml-js` npm module so you'll
// want to make sure your modifications will work with that package's
// `convert.js2xml` method
modifyXmlJs(xmlJs) {
xmlJs.rss.channel['itunes:author'] = 'Your name'
xmlJs.rss.channel['itunes:summary'] = 'Some other stuff'
return xmlJs
},
})
startServer
returns a promise with the started server in case that's useful.
It also ensures that the server is shut down properly if the process exits.
The server also supports rate limiting to help avoid people brute-forcing the username/password.
Project setup
In my project, I only need a few things to get this running: package.json
,
index.js
, and forever.config.json
package.json
:
This lists the dependencies and the scripts for the project.
{
"private": true,
"name": "doddsfam-audiobooks",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"stop": "FOREVER_ROOT=./.forever forever stopall",
"start": "FOREVER_ROOT=./.forever forever start ./forever.config.json"
},
"license": "UNLICENSED",
"dependencies": {
"@kentcdodds/podcastify-dir": "^1.4.2",
"forever": "^3.0.0"
}
}
forever.config.json
:
forever
is a module that will ensure that if the
server stops for any reason, it is automatically restarted. That way you don't
have to log into your server to restart it if it crashed. Here's how I configure
it:
{
"append": true,
"script": "index.js",
"sourceDir": ".",
"logFile": "./forever.log",
"outFile": "./out.log",
"errFile": "./error.log"
}
index.js
:
const {startServer} = require('@kentcdodds/podcastify-dir')
startServer({
title: 'Dodds Family Audiobooks',
description: 'The audiobooks of the Dodds family',
image: {
url: 'https://www.dropbox.com/s/some-id/some-filename.jpg?raw=1',
title: 'Dodds Family Audiobooks',
link: 'https://kentcdodds.com',
height: 500,
width: 500,
},
port: 8879,
directory: '/volume1/audiobooks/files',
users: {bob: 'the_builder'},
modifyXmlJs(xmlJs) {
xmlJs.rss.channel['itunes:author'] = 'Kent C. Dodds'
xmlJs.rss.channel['itunes:summary'] = 'Dodds Family Audiobooks'
return xmlJs
},
})
Because my NAS is accessible via the world-wide-web, I can use this URL in my podcast app:
http://bob:[email protected]:8879/audiobook/feed.xml
That's:
http://[username]:[password]@[domain_or_global_static_ip_address]:[port]/audiobook/feed.xml
I paste that into PocketCasts's submit page (specifying "private" so it's not indexed) to get a pocketcast URL for the podcast and then load that up in my pocketcasts account. I've had some success with other apps, but I've had some trouble with all of them. I recommend experimenting a bit.
Other exports
getPodcastMiddleware
and getPodcastRoutes
are also exported if you'd like to
use those more directly. I don't plan to document those, but feel free to
explore the source code if you need something more advanced.
Other info
query string
The /audiobook/feed.xml
endpoint allows you to filter and sort the audiobooks
via the query string. You can also specify a custom image. This allows you to
set up custom feeds for different categories of books. Here's a full example
(put on multiple lines to simplify reading it):
http://bob:[email protected]:8879/audiobook/feed.xml
?filterIn=fantasy:category
&filterOut=poppins:title,joe:author
&sort=desc:pubDate,asc:duration
&title=Fantasy%20books
&image.url=https%3A%2F%2Fwww.dropbox.com%2Fs%2Fsome-id%2Fsome-name.png%3Fraw%3D1
&image.title=epic%20fantasy
&image.link=https%3A%2F%2Fkentcdodds.com
&image.height=500
&image.width=740
&image.description=epic%20fantasy
So you have title
, filterIn
, filterOut
, sort
, and image.*
options.
The fitler query params are a list of comma-separated filter sets which is a
pair of [regex]:[property]
. The sort
is [direction]:[property]
.
Additionally, the filterIn
and filterOut
options can be used multiple times.
So if you want to filter in "self help" and "career" categories, then you could
use multiple filterIn
params. For both filterIn
and filterOut
, the item
must match all of the query options to apply.
Alternatively, you could create individual feeds by starting multiple servers on different ports and putting the audio files in different directories.
cache
Because reading the audiobook MP3 files for metadata can take some time (added
300ms to the request when testing on my MacBook with just a few books), we cache
the metadata in memory. This is a pretty significant perf savings. However, if
you add a new book, or change metadata about a book, you'll want to delete the
cache, so there's also a /audiobook/bust-cache
endpoint you can hit with a GET
request and it'll reset the cache.
Editing book metadata
I use Kid3 for editing book metadata. It works pretty well. If you're audiobooks come from a reputable source (most of my books come from Audible which I download using OpenAudible), all the metadata should be set properly already. If you need to edit things manually, here are the values you need to have set:
title
- book titlecomment
- book summaryasin
- book IDartist
- book authorduration
- the time duration of the audiobooknarrated_by
- the person (or people) who narrated the audiobook (optional)book_genre
orgenre
- A colon-separated list of applicable categories:Kids 8-10:Adventure:Fantasy
year
- The release date of the audiobook:2020-01-23
APIC
- The Cover art (Kid3 allows you to drag-and-drop an image).
Other Solutions
I'm not aware of any, if you are please make a pull request and add it here!
About @kentcdodds/
scoped and kcd-
prefixed packages
If a package I maintain is scoped to my username (@kentcdodds
) or prefixed
with kcd-
, that means I built and maintain it for myself. You're more than
welcome to use it, but I'm not likely to put much work into making it work for
other people's use cases (I'm not heartless, I just don't have the time). If you
have a grander vision for the project, please feel free to bring it up in the
comments and perhaps we can collaborate on that vision and make it more
general-purpose (and remove the scope/prefix), but it's possible I'll recommend
you just fork the project and publish your own version.
Issues
Looking to contribute? Look for the Good First Issue label.
🐛 Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
💡 Feature Requests
Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on.
Contributors ✨
Thanks goes to these people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
LICENSE
MIT