native support from Express
Express's sendFile supports byte-range requests out of the box (like send mentioned in the comments). You just need to add the 206 status code:
res.status(206);
res.sendFile(filepath);
debug
To debug whether your server is working, take these steps:
- Verify that the server supports byte-range requests by requesting 20 bytes; if the result is 21, you got the right number of bytes (taken from ServerFault):
$ curl --silent --range 20-40 https://emotionathletes.org/podcast/1.mp3 | wc -c
21
- test the response code in the head when you request the whole file:
$ curl --head https://emotionathletes.org/podcast/1.mp3
HTTP/1.1 206 Partial Content
X-Powered-By: Express
Accept-Ranges: bytes
...
- test the response code and the byte range when you request a range (this one may be redundant)
$ curl -H Range:bytes=20-40 -I https://emotionathletes.org/podcast/1.mp3
HTTP/1.1 206 Partial Content
X-Powered-By: Express
Accept-Ranges: bytes
...
You can also test this with wget instead of curl, e.g.:
wget --header="Range: bytes=20-40" -t 1 https://emotionathletes.org/podcast/1.mp3
You can install wget on macOS with brew.
still failing? Check the XML feed
If all this works and you still get a validation failure on iTunes connect, the problem may be you XML feed. Consider copying the structure of an XML feed that works on iTunes connect (such as mine), or ask for specific help.
In my case, I was serving podcasts identified with /podcast/1 and iTunes did not even try downloading them because they lacked an extension. iTunes requested the feed with user agents iTMS and Jakarta, but did not request the episodes. Comparing to a successful feed, I realized that I needed an extension, so I used that.
building the XML feed in NodeJS
You can also build the RSS feed from NodeJS with the rss package. The attributes are a little tricky, e.g. iTunes needs an enclosure with the url, so I copy my part of my podcast.js here. For simplicity, I compiled it into a static XML file with node podcast.js, then served like any other static file.
const fs = require('fs');
const path = require('path');
const RSS = require('rss');
/* lets create an rss feed */
var feed = new RSS({
itunesExplicit: false,
title: 'Ginja',
description: 'Um curso virtual que desenvolve a inteligência emocional nas crianças',
feed_url: 'https://emocoes.org/podcast-feed',
site_url: 'https://emocoes.org',
image_url: 'http://emocoes.org/images/logo_podcast.png',
managingEditor: 'Miguel Morin',
webMaster: 'Miguel Morin',
copyright: '2020 Atletismo Emocional',
language: 'pt',
categories: ['Education for Kids','Stories for Kids','Mental Health'],
pubDate: 'October 20, 2020 22:00:00 GMT',
ttl: '60',
custom_namespaces: {
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'
},
custom_elements: [
{'itunes:explicit': 'no'},
{'itunes:subtitle': 'Atletismo Emocional para crianças'},
{'itunes:author': 'Ginja'},
{'itunes:summary': 'Olá crianças! Eu sou o Ginja, o guru das emoções! Ao longo de várias aventuras com os meus amigos, vamos aprender a reconhecer, aceitar e utilizar as nossas emoções. E no fim, vamos ser uns atletas emocionais e chegar à meta da felicidade.\n\nVisitem a minha página: https://emocoes.org\n\nSigam-me no Facebook: https://facebook.com/emotionathletes\n\nSigam-me no Instagram: https://instagram.com/emotionathletes\n\nSigam-me no Twitter: https://twitter.com/emotionathletes'},
{'itunes:owner': [
{'itunes:name': 'Ginja'},
{'itunes:email': 'ginja@emocoes.org'}
]},
{'itunes:image': {
_attr: {
href: 'http://emocoes.org/images/logo_podcast.png'
}
}},
{'itunes:category': [
{_attr: {
text: 'Kids & Family'
}},
{'itunes:category': {
_attr: {
text: 'Education for Kids'
}
}}
]}
]
});
/* loop over data and add to feed */
feed.item({
title: '1: Bem-vindos!',
description: 'O Ginja explica que as emoções são como as ondas do mar. (Mensagem principal: As emoções são como as ondas do mar: vêm e vão, não as podemos parar.)',
url: 'https://emocoes.org/podcast/1.mp3', // link to the item
date: 'October 20, 2020 22:00:00 GMT', // any format that js Date can parse.
custom_elements: [
{'itunes:author': 'Ginja'},
{'itunes:subtitle': 'O Ginja explica que as emoções são como as ondas do mar. (Mensagem principal: As emoções são como as ondas do mar: vêm e vão, não as podemos parar.)'},
{'itunes:duration': '4:23'},
{'enclosure url="https://emocoes.org/podcast/1.mp3" length="8520238" type="audio/mpeg"': {}}
]
});
var xml = feed.xml({indent: true});
let filepath = path.join(__dirname, `./public/podcast.xml`)
fs.writeFile(filepath, xml, (err) => {if (err) throw err;});
other options
The comments mentioned nginx as a podcast server. I could not find how to do it and asked about it on ServerFault.