eleventy-plugin-podcaster 2.0.0-alpha.4 → 2.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/eleventy.config.js +2 -0
- package/package.json +1 -1
- package/src/calculateEpisodeSizeAndDuration.js +5 -26
- package/src/chapters.js +13 -0
- package/src/chapters.njk +9 -0
- package/src/podcastFeed.js +25 -2
- package/src/podcastFeed.njk +4 -0
- package/src/readableDuration.js +26 -0
- package/.env +0 -5
- package/ava.config.js +0 -3
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# eleventy-plugin-podcaster 🕚⚡️🎈🐀🎤📲
|
|
2
2
|
|
|
3
|
-
`eleventy-plugin-podcaster` — or **Podcaster**, as we will call it from now on — is an Eleventy plugin which lets you create a podcast and its accompanying website.
|
|
3
|
+
`eleventy-plugin-podcaster` — or **Podcaster**, as we will call it from now on — is an Eleventy plugin which lets you create a podcast and its accompanying website.
|
|
4
|
+
|
|
5
|
+
- **Podcaster** creates the podcast feed that you submit to Apple Podcasts, Spotify or any other podcast directory.
|
|
6
|
+
- **Podcaster** also provides information about your podcast to your Eleventy templates. This means that you can describe the podcast and its episodes on your podcast website, creating pages for individual episodes, guests, topics, seasons or anything else at all.
|
|
4
7
|
|
|
5
8
|
Plenty of services exist that will host your podcast online — [Spotify][], [Acast][], [Podbean][], [Buzzsprout][], [Blubrry][]. But none of these will allow you to own your podcast's presence on the web, and none of them will give you the freedom to create a site that presents your podcast in a way that reflects its premise, tone and style.
|
|
6
9
|
|
|
@@ -44,9 +47,9 @@ Detailed and specific information about how to install and use **Podcaster** can
|
|
|
44
47
|
|
|
45
48
|
1. **Podcaster** is an Eleventy plugin. Create an Eleventy site and install the `eleventy-plugin-podcaster` plugin in the usual way.
|
|
46
49
|
2. In the data directory, create a `podcast.json` file. This will contain information about your podcast and its site — at the very least, its title, the URL of the site, a description, its language, and its category.
|
|
47
|
-
3. In the input directory, create
|
|
48
|
-
4. In the input directory, create
|
|
49
|
-
5. Create pages on your site for each of your episodes using the posts in the `
|
|
50
|
+
3. In the input directory, create an `episode-files` directory and put your podcast MP3s in there.
|
|
51
|
+
4. In the input directory, create an `episode-posts` directory. Here you will create a post for each episode, which will include information about the episode in its filename and front matter and which whill contain the episode description or show notes.
|
|
52
|
+
5. Create pages on your site for each of your episodes using the posts in the `episode-posts` directory. And you can also create index pages, topic pages, guest pages or anything you like — all using the information you have supplied to **Podcaster** in your `podcast.json` file and your episode posts.
|
|
50
53
|
6. Find a host for your site, and a CDN for your podcast episodes.
|
|
51
54
|
|
|
52
55
|
> [!WARNING]
|
package/eleventy.config.js
CHANGED
|
@@ -7,6 +7,7 @@ import calculateEpisodeSizeAndDuration from './src/calculateEpisodeSizeAndDurati
|
|
|
7
7
|
import calculateEpisodeFilename from './src/calculateEpisodeFilename.js'
|
|
8
8
|
|
|
9
9
|
import readableFilters from './src/readableFilters.js'
|
|
10
|
+
import chapters from './src/chapters.js'
|
|
10
11
|
import excerpts from './src/excerpts.js'
|
|
11
12
|
import drafts from './src/drafts.js'
|
|
12
13
|
import pageTitle from './src/pageTitle.js'
|
|
@@ -26,6 +27,7 @@ export default function (eleventyConfig, options = {}) {
|
|
|
26
27
|
// Filters
|
|
27
28
|
|
|
28
29
|
eleventyConfig.addPlugin(readableFilters, options)
|
|
30
|
+
eleventyConfig.addPlugin(chapters, options)
|
|
29
31
|
|
|
30
32
|
// Optional features
|
|
31
33
|
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
import readableDuration from './readableDuration.js'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { existsSync } from 'node:fs'
|
|
4
5
|
import { readdir, stat, writeFile } from 'node:fs/promises'
|
|
@@ -11,29 +12,6 @@ import isEpisodePost from './isEpisodePost.js'
|
|
|
11
12
|
const isAudioFile = episodeFilename => episodeFilename.endsWith('.mp3') ||
|
|
12
13
|
episodeFilename.endsWith('.m4a')
|
|
13
14
|
|
|
14
|
-
const convertSecondsToReadableDuration = seconds =>
|
|
15
|
-
Duration.fromMillis(seconds * 1000)
|
|
16
|
-
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
|
17
|
-
.toHuman()
|
|
18
|
-
|
|
19
|
-
const convertReadableDurationToSeconds = duration => {
|
|
20
|
-
const durationPattern = /^(?:(?<hours>\d+):)?(?<minutes>\d{1,2}):(?<seconds>\d{2}(?:\.\d+)?)$/
|
|
21
|
-
|
|
22
|
-
let match
|
|
23
|
-
if (duration?.match) {
|
|
24
|
-
match = duration.match(durationPattern)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (match) {
|
|
28
|
-
const hours = isNaN(parseInt(match.groups.hours))
|
|
29
|
-
? 0
|
|
30
|
-
: parseInt(match.groups.hours)
|
|
31
|
-
const minutes = parseInt(match.groups.minutes)
|
|
32
|
-
const seconds = parseFloat(match.groups.seconds)
|
|
33
|
-
return hours * 3600 + minutes * 60 + seconds
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
15
|
async function readEpisodeDataLocally (episodeFilesDirectory) {
|
|
38
16
|
const episodes = await readdir(episodeFilesDirectory)
|
|
39
17
|
const episodeData = {}
|
|
@@ -62,7 +40,7 @@ function calculatePodcastData (episodeData) {
|
|
|
62
40
|
|
|
63
41
|
function reportPodcastData (podcastData) {
|
|
64
42
|
const { numberOfEpisodes, totalSize, totalDuration } = podcastData
|
|
65
|
-
console.log(`\u001b[33m${numberOfEpisodes} episodes; ${hr.fromBytes(totalSize)}; ${
|
|
43
|
+
console.log(`\u001b[33m${numberOfEpisodes} episodes; ${hr.fromBytes(totalSize)}; ${readableDuration.convertFromSeconds(totalDuration)}.\u001b[0m`)
|
|
66
44
|
}
|
|
67
45
|
|
|
68
46
|
async function writePodcastDataLocally (episodeData, podcastData, directories) {
|
|
@@ -164,6 +142,7 @@ export default function (eleventyConfig, options = {}) {
|
|
|
164
142
|
let firstRun = true
|
|
165
143
|
eleventyConfig.on('eleventy.before', async ({ directories }) => {
|
|
166
144
|
if (!firstRun || process.env.SKIP_EPISODE_CALCULATIONS === 'true') return
|
|
145
|
+
|
|
167
146
|
firstRun = false
|
|
168
147
|
|
|
169
148
|
const episodeFilesDirectory = options.episodeFilesDirectory
|
|
@@ -195,7 +174,7 @@ export default function (eleventyConfig, options = {}) {
|
|
|
195
174
|
eleventyConfig.addGlobalData('eleventyComputed.episode.duration', () => {
|
|
196
175
|
return data => {
|
|
197
176
|
if (data.episode.duration) {
|
|
198
|
-
const convertedReadableDuration =
|
|
177
|
+
const convertedReadableDuration = readableDuration.convertToSeconds(data.episode.duration)
|
|
199
178
|
return convertedReadableDuration ?? data.episode.duration
|
|
200
179
|
}
|
|
201
180
|
|
package/src/chapters.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import readableDuration from './readableDuration.js'
|
|
2
|
+
|
|
3
|
+
export default (eleventyConfig, options = {}) => {
|
|
4
|
+
eleventyConfig.addFilter('normalizeChaptersData', (data) => {
|
|
5
|
+
const result = data.map(x => {
|
|
6
|
+
if (x.startTime) {
|
|
7
|
+
x.startTime = readableDuration.convertToSeconds(x.startTime) ?? x.startTime
|
|
8
|
+
}
|
|
9
|
+
return x
|
|
10
|
+
})
|
|
11
|
+
return JSON.stringify(result, null, 2)
|
|
12
|
+
})
|
|
13
|
+
}
|
package/src/chapters.njk
ADDED
package/src/podcastFeed.js
CHANGED
|
@@ -9,7 +9,6 @@ export default function (eleventyConfig, options = {}) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const podcastFeedPath = path.join(import.meta.dirname, './podcastFeed.njk')
|
|
12
|
-
|
|
13
12
|
eleventyConfig.addTemplate('feed.njk', readFileSync(podcastFeedPath), {
|
|
14
13
|
eleventyExcludeFromCollections: true,
|
|
15
14
|
eleventyImport: {
|
|
@@ -17,6 +16,14 @@ export default function (eleventyConfig, options = {}) {
|
|
|
17
16
|
}
|
|
18
17
|
})
|
|
19
18
|
|
|
19
|
+
const chaptersPath = path.join(import.meta.dirname, './chapters.njk')
|
|
20
|
+
eleventyConfig.addTemplate('chapters.njk', readFileSync(chaptersPath), {
|
|
21
|
+
eleventyExcludeFromCollections: true,
|
|
22
|
+
eleventyImport: {
|
|
23
|
+
collections: ['episodePostWithChapters']
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
20
27
|
eleventyConfig.addPlugin(rssPlugin, {
|
|
21
28
|
posthtmlRenderOptions: {
|
|
22
29
|
closingSingleTag: 'default' // opt-out of <img/>-style XHTML single tags
|
|
@@ -24,6 +31,22 @@ export default function (eleventyConfig, options = {}) {
|
|
|
24
31
|
})
|
|
25
32
|
|
|
26
33
|
eleventyConfig.addCollection('episodePost', (collectionApi) => {
|
|
27
|
-
return collectionApi
|
|
34
|
+
return collectionApi
|
|
35
|
+
.getAll()
|
|
36
|
+
.filter(item => isEpisodePost(item.data, options))
|
|
37
|
+
// sort order explicit: presence of template data files disrupts it otherwise
|
|
38
|
+
.sort((a, b) => a.date - b.date)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// included for backward compatibility, will be deprecated in 3.0
|
|
42
|
+
eleventyConfig.addCollection('podcastEpisode', (collectionApi) => {
|
|
43
|
+
return collectionApi
|
|
44
|
+
.getAll()
|
|
45
|
+
.filter(item => isEpisodePost(item.data, options))
|
|
46
|
+
.sort((a, b) => a.date - b.date)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
eleventyConfig.addCollection('episodePostWithChapters', (collectionApi) => {
|
|
50
|
+
return collectionApi.getAll().filter(item => isEpisodePost(item.data, options) && item.data.episode?.chapters)
|
|
28
51
|
})
|
|
29
52
|
}
|
package/src/podcastFeed.njk
CHANGED
|
@@ -60,6 +60,10 @@ eleventyAllowMissingExtension: true
|
|
|
60
60
|
<guid isPermaLink="true">{{ post.url | htmlBaseUrl(siteUrl) }}</guid>
|
|
61
61
|
{% endif -%}
|
|
62
62
|
<pubDate>{{ post.date | dateToRfc3339 }}</pubDate>
|
|
63
|
+
{%- if post.data.episode.chapters %}
|
|
64
|
+
<podcast:chapters url="{{ [post.url, 'chapters.json'] | join('/') | htmlBaseUrl(siteUrl) }}"
|
|
65
|
+
type="application/json+chapters" />
|
|
66
|
+
{% endif -%}
|
|
63
67
|
{% if podcast.episodeDescriptionTemplate %}
|
|
64
68
|
{%- set episodeDescription -%}
|
|
65
69
|
{% include podcast.episodeDescriptionTemplate %}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Duration } from 'luxon'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
convertFromSeconds (seconds) {
|
|
5
|
+
return Duration.fromMillis(seconds * 1000)
|
|
6
|
+
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
|
7
|
+
.toHuman()
|
|
8
|
+
},
|
|
9
|
+
convertToSeconds (duration) {
|
|
10
|
+
const durationPattern = /^(?:(?<hours>\d+):)?(?<minutes>\d{1,2}):(?<seconds>\d{2}(?:\.\d+)?)$/
|
|
11
|
+
|
|
12
|
+
let match
|
|
13
|
+
if (duration?.match) {
|
|
14
|
+
match = duration.match(durationPattern)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (match) {
|
|
18
|
+
const hours = isNaN(parseInt(match.groups.hours))
|
|
19
|
+
? 0
|
|
20
|
+
: parseInt(match.groups.hours)
|
|
21
|
+
const minutes = parseInt(match.groups.minutes)
|
|
22
|
+
const seconds = parseFloat(match.groups.seconds)
|
|
23
|
+
return hours * 3600 + minutes * 60 + seconds
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/.env
DELETED
package/ava.config.js
DELETED