Tons of work on Grav 1.7 compatibility. Theming updates started

This commit is contained in:
Jeremy Gonyea
2021-01-24 00:46:35 -05:00
parent dec2809739
commit 5a36021864
21 changed files with 337 additions and 410 deletions

View File

@@ -1,3 +1,17 @@
# v3.0.0
## 01/23/2021
1. [](#new)
* Grav v1.7 support. Grav v1.6 is no longer supported.
* Individual file field uploads are no longer supported in Grav v1.7. Instead, use the general media upoader and then the filepicker fields to select the appropriate audio/ image files.
2. [](#improved)
* Better multi-language support while creating new content via the admin plugin.
* Twig formatting is improved.
* Updated README to indicate that admin is now required. If this becomes a problem, I can revisit the requirements.
* Removed `max_upload` audio file size value from plugin configuration.
3. [](#bugfix)
* Episode subtitle in the rss feed now points to the correct field in the admin form. You may need to re-save this data on v2 Podcast episodes.
# v2.1.10 # v2.1.10
## 01/21/2021 ## 01/21/2021

View File

@@ -1,7 +1,7 @@
# Podcast Plugin # Podcast Plugin
The **Podcast** Plugin is for [Grav CMS](http://github.com/getgrav/grav). This plugin creates the following: The **Podcast** Plugin is for [Grav CMS](http://github.com/getgrav/grav). This plugin creates the following:
- Page templates for Podcast Channel, Podcast Series, and Podcast Episode - Admin Page templates for Podcast Channel, Podcast Series, and Podcast Episode
- An iTunes compatible podcast RSS feed, both at the Podcast Channel (all episodes) and Podcast Series (only a series' episodes) - An iTunes compatible podcast RSS feed, both at the Podcast Channel (all episodes) and Podcast Series (only a series' episodes)
## Installation ## Installation
@@ -25,14 +25,13 @@ You should now have all the plugin files under
/your/site/grav/user/plugins/podcast /your/site/grav/user/plugins/podcast
> NOTE: This plugin is a modular component for Grav which requires the following to operate: > NOTE: This plugin is a modular component for Grav which requires the following to operate:
* [Grav Core](http://github.com/getgrav/grav)
* [Admin](https://github.com/getgrav/grav-plugin-admin)
* [Auto Date](https://github.com/getgrav/grav-plugin-auto-date) * [Auto Date](https://github.com/getgrav/grav-plugin-auto-date)
* [Error](https://github.com/getgrav/grav-plugin-error)
* [Feed](https://github.com/getgrav/grav-plugin-feed) * [Feed](https://github.com/getgrav/grav-plugin-feed)
* [GetId3](https://github.com/jgonyea/grav-plugin-get-id3), along with its accompanying [getID3 php library](http://www.getid3.org/) * [GetId3](https://github.com/jgonyea/grav-plugin-get-id3), along with its accompanying [getID3 php library](http://www.getid3.org/)
* [Grav](http://github.com/getgrav/grav)
* [Problems](https://github.com/getgrav/grav-plugin-problems)
> While technically not required, using the [Admin](https://github.com/getgrav/grav-plugin-admin) plugin will assist in adding new content. ...and any other required plugins the above list requires.
## Configuration ## Configuration

View File

@@ -1,8 +0,0 @@
# A sample Gemfile
source "https://rubygems.org"
gem 'compass', ">=1.0.3"
gem "sassy-buttons", ">=0.1.4"
gem "breakpoint", ">=2.4.0"
gem "compass-rgbapng", ">=0.2.1"
gem "autoprefixer-rails"
gem "ffi", ">= 1.9.24"

View File

@@ -1,49 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
autoprefixer-rails (9.4.7)
execjs
breakpoint (2.7.1)
sass (~> 3.3)
sassy-maps (< 1.0.0)
chunky_png (1.3.11)
compass (1.0.3)
chunky_png (~> 1.2)
compass-core (~> 1.0.2)
compass-import-once (~> 1.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
sass (>= 3.3.13, < 3.5)
compass-core (1.0.3)
multi_json (~> 1.0)
sass (>= 3.3.0, < 3.5)
compass-import-once (1.0.5)
sass (>= 3.2, < 3.5)
compass-rgbapng (0.2.1)
chunky_png (>= 0.8.0)
compass (>= 0.10.0)
execjs (2.7.0)
ffi (1.10.0-x64-mingw32)
multi_json (1.13.1)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
sass (3.4.25)
sassy-buttons (0.2.6)
compass (>= 0.12.2)
sassy-maps (0.4.0)
sass (~> 3.3)
PLATFORMS
x64-mingw32
DEPENDENCIES
autoprefixer-rails
breakpoint (>= 2.4.0)
compass (>= 1.0.3)
compass-rgbapng (>= 0.2.1)
ffi (>= 1.9.24)
sassy-buttons (>= 0.1.4)
BUNDLED WITH
2.0.1

View File

@@ -1,69 +0,0 @@
#
# This file is only needed for Compass/Sass integration. If you are not using
# Compass, you may safely ignore or delete this file.
#
# If you'd like to learn more about Sass and Compass, see the sass/README.txt
# file for more information.
#
# Suppressing warnings due to old Zen theme and deprecated legacy browser support.
disable_warnings = true
# Change this to :production when ready to deploy the CSS to the live server.
#environment = :development
environment = :production
# Create sourcemaps
sourcemap = true
# In development, we can turn on the FireSass-compatible debug_info.
#firesass = false
firesass = false
Sass::Plugin.options[:debug_info] = false
# Location of the theme's resources.
css_dir = "css"
sass_dir = "sass"
fonts_dir = "css/fonts"
extensions_dir = "sass-extensions"
images_dir = "images"
javascripts_dir = "js"
cache_path = '/tmp/.sass-cache'
# Require any additional compass plugins installed on your system.
require 'autoprefixer-rails'
require 'sassy-buttons'
require 'breakpoint'
require "rgbapng"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
output_style = (environment == :development) ? :expanded : :compressed
# To enable relative paths to assets via compass helper functions. Since Drupal
# themes can be installed in multiple locations, we don't need to worry about
# the absolute path to the theme from the server root.
relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
line_comments = false
# Pass options to sass. For development, we turn on the FireSass-compatible
# debug_info if the firesass config variable above is true.
# sass_options = (environment == :development && firesass == true) ? {:debug_info => true} : {}
# Run autoprefixer after compass compiles
# Using on_stylesheet_saved post-compile hook
on_stylesheet_saved do |file|
css = File.read(file)
File.open(file, 'w') { |io| io << AutoprefixerRails.process(css, browsers: ['> 20%', 'IE 10'], remove: false) }
cssfile = File.basename file
# Append sourcemap reference that autoprefixer removed to css file.
system( "printf '/*# sourceMappingURL=" + cssfile + ".map */' >> " + File.absolute_path(file) )
system( "echo '" + cssfile + ".map sourcemap reference appended to '" + cssfile )
system( "echo 'Autoprefixer has processed " + cssfile + "'" )
end

View File

@@ -1,2 +1,36 @@
.podcast-channel-header{width:100%;clear:both;display:inline-block;border-bottom:3px solid #000}.podcast-channel-image{width:25%;float:left;display:inline-block;border:1px solid #000}.channel-meta{width:74%;float:left;display:inline-block}.channel-meta h1{text-align:center;margin:0}.channel-meta h2{text-align:center}.channel-meta p.owner{text-align:center;margin:-1em 0 0 0}.channel-meta p.description{margin-left:auto;margin-right:auto;text-align:center;line-height:1.2}.channel-links{float:left;margin-left:2em}.channel-content{display:inline-block}#episodes{display:inline-block;clear:both;width:75%;float:left}#episodes ul{list-style-type:none}.clearfix{clear:both}#podcast-series{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;display:inline-block;background:#ccc;width:24%;float:right;min-height:16em}#podcast-series h2{font-size:20px;text-align:center}#podcast-series a{color:#000}#podcast-series a:hover{font-weight:bold} .episode-container h1,h2{
/*# sourceMappingURL=podcast.css.map */ text-align: center;
margin: 0;
padding: 0;
line-height: 40px;
}
.episode-flex-wrapper{
display: flex;
flex-flow: row wrap;
}
.episode-flex-child {
flex: 1;
border: 2px solid yellow;
padding: 1.5em;
}
.episode-flex-child h3 {
text-align: center;
}
.episode-top {
flex: 0 0 100%;
text-align: center;
}
.episode-content {
flex: 4;
}
.episode-nav {
border-bottom: 1px solid #3A414E;
}
@media all and (max-width:800px){
.episode-flex-wrapper{
flex-flow: column wrap;
}
}

View File

@@ -1,7 +0,0 @@
{
"version": 3,
"mappings": "AAQA,uBAAwB,CACpB,KAAK,CAAE,IAAI,CACX,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,YAAY,CACrB,aAAa,CAAE,cAAc,CAEjC,sBAAuB,CACnB,KAAK,CAAE,GAAG,CACV,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,cAAc,CAG1B,aAAc,CACV,KAAK,CAAE,GAAG,CACV,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,YAAY,CACrB,gBAAG,CACC,UAAU,CAAE,MAAM,CAClB,MAAM,CAAE,CAAC,CAEb,gBAAG,CACC,UAAU,CAAE,MAAM,CAEtB,qBAAQ,CACJ,UAAU,CAAE,MAAM,CAClB,MAAM,CAAE,UAAU,CAGtB,2BAAc,CACV,WAAW,CAAE,IAAI,CACjB,YAAY,CAAE,IAAI,CAClB,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAIxB,cAAe,CACX,KAAK,CAAE,IAAI,CACX,WAAW,CAAE,GAAG,CAIpB,gBAAiB,CACb,OAAO,CAAE,YAAY,CAGzB,SAAU,CACN,OAAO,CAAE,YAAY,CACrB,KAAK,CAAE,IAAI,CACX,KAAK,CAAE,GAAG,CACV,KAAK,CAAE,IAAI,CACX,YAAG,CACC,eAAe,CAAE,IAAI,CAI7B,SAAU,CACN,KAAK,CAAE,IAAI,CAGf,eAAgB,CCuRd,kBAAwC,CDtRf,GAAG,CCsR5B,qBAAwC,CC9Sb,GAAuB,CD8SlD,aAAwC,CDtRf,GAAG,CAC1B,OAAO,CAAE,YAAY,CACrB,UAAU,CAlEM,IAAI,CAmEpB,KAAK,CAAE,GAAG,CACV,KAAK,CAAE,KAAK,CACZ,UAAU,CAAE,IAAI,CAChB,kBAAG,CACC,SAAS,CAAE,IAAI,CACf,UAAU,CAAE,MAAM,CAEtB,iBAAE,CACE,KAAK,CA5EO,IAAI,CA8EpB,uBAAQ,CACJ,WAAW,CAAE,IAAI",
"sources": ["../sass/podcast.scss","../../../../../../../../../xampp/ruby/lib/ruby/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/_support.scss","../../../../../../../../../xampp/ruby/lib/ruby/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/css3/_border-radius.scss"],
"names": [],
"file": "podcast.css"
}

View File

@@ -1,87 +0,0 @@
/* Styling for podcast related content */
@import "compass/css3";
@import "compass/utilities";
$series-foreground: #000;
$series-background: #ccc;
.podcast-channel-header {
width: 100%;
clear: both;
display: inline-block;
border-bottom: 3px solid #000;
}
.podcast-channel-image {
width: 25%;
float: left;
display: inline-block;
border: 1px solid #000;
}
.channel-meta {
width: 74%;
float: left;
display: inline-block;
h1 {
text-align: center;
margin: 0;
}
h2 {
text-align: center;
}
p.owner {
text-align: center;
margin: -1em 0 0 0;
}
p.description {
margin-left: auto;
margin-right: auto;
text-align: center;
line-height: 1.2;
}
}
.channel-links {
float: left;
margin-left: 2em;
}
.channel-content {
display: inline-block;
}
#episodes {
display: inline-block;
clear: both;
width: 75%;
float: left;
ul {
list-style-type: none;
}
}
.clearfix {
clear: both;
}
#podcast-series {
@include border-radius(2px);
display: inline-block;
background: $series-background;
width: 24%;
float: right;
min-height: 16em;
h2 {
font-size: 20px;
text-align: center;
}
a {
color: $series-foreground;
}
a:hover {
font-weight: bold;
}
}

View File

@@ -1,5 +1,5 @@
name: Podcast name: Podcast
version: 2.1.10 version: 3.0.0
description: Creates Podcast page types and related podcast RSS feeds description: Creates Podcast page types and related podcast RSS feeds
icon: microphone icon: microphone
author: author:
@@ -12,6 +12,7 @@ docs: https://github.com/jgonyea/grav-plugin-podcast/blob/develop/README.md
license: MIT license: MIT
dependencies: dependencies:
- admin
- auto-date - auto-date
- feed - feed
- get-id3 - get-id3
@@ -29,8 +30,3 @@ form:
0: PLUGIN_ADMIN.DISABLED 0: PLUGIN_ADMIN.DISABLED
validate: validate:
type: bool type: bool
max_upload:
type: text
label: PLUGIN_PODCAST.CONFIG.MAX_UPLOAD_LABEL
help: PLUGIN_PODCAST.CONFIG.MAX_UPLOAD_HELP
default: 50

View File

@@ -12,13 +12,11 @@ form:
content: content:
type: tab type: tab
ordering@: 0 ordering@: 0
title: Podcast Page Content title: PLUGIN_PODCAST.ADMIN.CHANNEL.CONTENT.TAB_TITLE
fields: fields:
header.media_order:
unset@: true
header.title: header.title:
type: text type: text
label: Page Title label: PLUGIN_PODCAST.ADMIN.CHANNEL.CONTENT.PAGE_TITLE_LABEL
header.feed.rss: header.feed.rss:
type: hidden type: hidden
@@ -30,84 +28,84 @@ form:
channelMetaTab: channelMetaTab:
type: tab type: tab
ordering@: 1 ordering@: 1
title: Channel Meta title: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.TAB_TITLE
fields: fields:
helptext: helptext:
type: spacer type: spacer
title: title:
text: Metadata related to the podcast's channel section. text: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.TAB_HELPTEXT
underline: true underline: true
uploads:
unset@: true
header.podcast.title: header.podcast.title:
type: text type: text
label: Podcast Title label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_TITLE_LABEL
help: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_TITLE_HELPTEXT
header.podcast.itunes.subtitle: header.podcast.itunes.subtitle:
type: text type: text
label: iTunes Subtitle label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_SUBTITLE_LABEL
help: 'Example: "A show about everything..."' help: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_SUBTITLE_HELPTEXT
header.podcast.description: header.podcast.description:
type: markdown type: markdown
label: Channel Description label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_DESCRIPTION_LABEL
ordering@: 0
header.podcast.link: header.podcast.link:
type: text type: text
label: Channel URL label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_LINK_LABEL
help: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_LINK_HELPTEXT
default: 'https://www.example.com' default: 'https://www.example.com'
header.podcast.channelLanguage: header.podcast.channelLanguage:
type: select type: select
label: Channel Language label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_LANG_LABEL
help: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_LANG_HELPTEXT
default: 'en' default: 'en'
data-options@: '\Grav\Plugin\PodcastPlugin::getLanguageOptions' data-options@: '\Grav\Plugin\PodcastPlugin::getLanguageOptions'
header.podcast.copyright: header.podcast.copyright:
type: text type: text
label: Copyright label: PLUGIN_PODCAST.ADMIN.CHANNEL.CHANNEL_META.PODCAST_COPYRIGHT_LABEL
default: 2017 Example.com default: 2021 Example.com
itunesMetaTab: itunesMetaTab:
type: tab type: tab
ordering@: 2 ordering@: 2
title: iTunes Meta title: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.TAB_TITLE
fields: fields:
helptext: helptext:
type: spacer type: spacer
title: title:
text: Metadata related to the podcast's itunes section. text: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.TAB_HELPTEXT
underline: true underline: true
header.podcast.itunes.author: header.podcast.itunes.author:
type: text type: text
label: Author label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_AUTHOR_LABEL
help: 'Example: "John Doe"' help: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_AUTHOR_HELPTEXT
header.podcast.itunes.owner.name: header.podcast.itunes.owner.name:
type: text type: text
label: Owner Name label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_OWNER_LABEL
help: 'Example: "John Doe"' help: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_OWNER_HELPTEXT
header.podcast.itunes.owner.email: header.podcast.itunes.owner.email:
type: email type: email
label: Owner Email label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_OWNER_EMAIL_LABEL
help: 'Example: "jdoe@example.com"' help: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_OWNER_EMAIL_HELPTEXT
header.podcast.itunes.image: header.podcast.itunes.image:
type: file type: filepicker
label: Channel Image label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_IMAGE_LABEL
destination: 'self@'
multiple: false
fileszie: 5
accept: accept:
- image/* - .bmp
- .png
- .jpg
- .jpeg
header.podcast.itunes.category: header.podcast.itunes.category:
type: select type: select
label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_CATEGORY_LABEL
size: long size: long
classes: fancy classes: fancy
label: Category
data-options@: '\Grav\Plugin\PodcastPlugin::getCategoryOptions' data-options@: '\Grav\Plugin\PodcastPlugin::getCategoryOptions'
header.podcast.itunes.subcategory: header.podcast.itunes.subcategory:
type: select type: select
label: Sub-Category label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_SUBCATEGORY_LABEL
data-options@: '\Grav\Plugin\PodcastPlugin::getSubCategoryOptions' data-options@: '\Grav\Plugin\PodcastPlugin::getSubCategoryOptions'
header.podcast.itunes.explicit: header.podcast.itunes.explicit:
type: toggle type: toggle
label: Explicit Content label: PLUGIN_PODCAST.ADMIN.CHANNEL.ITUNES_META.ITUNES_EXPLICIT_LABEL
highlight: 0 highlight: 0
default: no default: no
help: Does this podcast contain content that might be inappropriate for children? help: Does this podcast contain content that might be inappropriate for children?

View File

@@ -11,78 +11,75 @@ form:
fields: fields:
content: content:
type: tab type: tab
title: Podcast title: PLUGIN_PODCAST.ADMIN.EPISODE.CONTENT.TAB_TITLE
fields: fields:
header.title: header.title:
ordering@: 1 ordering@: 1
type: text type: text
label: Episode Title label: PLUGIN_PODCAST.ADMIN.EPISODE.CONTENT.PAGE_TITLE_LABEL
header.subtitle: header.podcast.itunes.subtitle:
ordering@: 2 ordering@: 2
type: text type: text
label: Episode subtitle label: PLUGIN_PODCAST.ADMIN.EPISODE.CONTENT.SUBTITLE_LABEL
header.podcast.episode_number: header.podcast.episode_number:
ordering@: 3 ordering@: 3
type: text type: text
label: Episode number label: PLUGIN_PODCAST.ADMIN.EPISODE.CONTENT.EPISODE_NUMBER_LABEL
size: x-small size: x-small
header.media_order:
unset@: true
podcastAudio: podcastAudio:
type: tab type: tab
title: Podcast Audio title: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.TAB_TITLE
ordering@: 1 ordering@: 1
fields: fields:
locally_hosted: locally_hosted:
type: spacer type: spacer
title: Locally Hosted Files title: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.LOCAL_HOSTED_LABEL
text: If you host your audio files on <strong>this</strong> server, upload the media below text: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.LOVAL_HOSTED_HELPTEXT
header.podcast.audio.local: header.podcast.audio.local.select:
type: file type: filepicker
label: Local Podcast Audio label: Local Podcast Audio
destination: '@self'
multiple: false
config-filesize@: plugins.podcast.max_upload
limit: 1
accept: accept:
- 'audio/*' - .mp3
- .wav
- .ogg
external_cdn: external_cdn:
type: spacer type: spacer
title: External Files title: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.EXTERNAL_HOSTED_LABEL
text: If you host your audio files on <strong>another</strong> server/CDN, fill in the field below text: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.EXTERNAL_HOSTED_HELPTEXT
header.podcast.audio.remote: header.podcast.audio.remote:
type: text type: text
label: Remote Podcast URL label: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.EXTERNAL_URL_LABEL
help: Remote files will take precendence over locally hosted when viewed on a page. help: PLUGIN_PODCAST.ADMIN.EPISODE.PODCAST_AUDIO.EXTERNAL_URL_HELPTEXT
itunesMetaTab: itunesMetaTab:
type: tab type: tab
ordering@: 2 ordering@: 2
title: iTunes Episode Meta title: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.TAB_TITLE
fields: fields:
helptext: helptext:
type: spacer type: spacer
title: text: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.TAB_HELPTEXT
text: Metadata related to the podcast episode
underline: true underline: true
header.podcast.itunes.author: header.podcast.itunes.author:
type: text type: text
label: Episode Author label: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.AUTHOR_LABEL
help: 'Example: "John Doe"' help: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.AUTHOR_HELPTEXT
header.podcast.itunes.image: header.podcast.itunes.image:
type: file type: filepicker
label: Episode Image label: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.IMAGE_LABEL
destination: 'self@' help: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.IMAGE_HELPTEXT
multiple: false
filesize: 5
accept: accept:
- image/* - .png
- .bmp
- .jpeg
- .jpg
header.podcast.itunes.explicit: header.podcast.itunes.explicit:
type: toggle type: toggle
label: Explicit Content label: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.EXPLICIT_LABEL
highlight: 0 help: PLUGIN_PODCAST.ADMIN.EPISODE.ITUNES_META.EXPLICIT_HELPTEXT
default: no default: no
help: Does this podcast contain content that might be inappropriate for children?
options: options:
yes: Yes yes: PLUGIN_ADMIN.YES
no: No no: PLUGIN_ADMIN.NO
validate:
type: bool

View File

@@ -12,27 +12,24 @@ form:
content: content:
type: tab type: tab
ordering@: 0 ordering@: 0
title: Podcast Series Content title: PLUGIN_PODCAST.ADMIN.SERIES.CONTENT.TAB_TITLE
fields: fields:
header.media_order:
unset@: true
header.title: header.title:
type: text type: text
label: Series Title label: PLUGIN_PODCAST.ADMIN.SERIES.CONTENT.PAGE_TITLE_LABEL
header.feed.rss: header.feed.rss:
type: hidden type: hidden
default: 'true' default: 'true'
header.feed.items: header.feed.items:
type: hidden type: hidden
default: '@self.children' default: '@self.children'
header.series.image: header.series.image:
type: file type: filepicker
@ordering: 2 label: PLUGIN_PODCAST.ADMIN.SERIES.CONTENT.SERIES_IMAGE_LABEL
label: Series Image help: PLUGIN_PODCAST.ADMIN.SERIES.CONTENT.SERIES_IMAGE_HELPTEXT
destination: 'self@' ordering@: 2
multiple: false
fileszie: 5
accept: accept:
- image/* - .png
- .bmp
- .jpg
- .jpeg

View File

@@ -4,4 +4,70 @@ en:
MAX_UPLOAD_LABEL: Max Podcast Filesize (MB) MAX_UPLOAD_LABEL: Max Podcast Filesize (MB)
MAX_UPLOAD_HELP: 'Set "upload_max_filesize" and "post_max_size" in php.ini to at least this value or higher.' MAX_UPLOAD_HELP: 'Set "upload_max_filesize" and "post_max_size" in php.ini to at least this value or higher.'
PODCAST: Podcast Channel PODCAST: Podcast Channel
SERIES: Podcast Series SERIES: Podcast Series
EPISODE_CONTENT:
HEADER: Episode Content
DOWNLOAD: Download Audio
WARNING: Your browser does not support the audio tag.
OTHER: Other Episodes In
ADMIN:
CHANNEL:
CONTENT:
TAB_TITLE: Podcast Page Content
PAGE_TITLE_LABEL: Page Title
CHANNEL_META:
TAB_TITLE: Channel Meta
TAB_HELPTEXT: Metadata related to the podcast's channel section.
PODCAST_TITLE_LABEL: Podcast Title
PODCAST_TITLE_HELPTEXT: Used in the RSS feed.
PODCAST_SUBTITLE_LABEL: iTunes Subtitle
PODCAST_SUBTITLE_HELPTEXT: 'Example: "A show about everything..."'
PODCAST_DESCRIPTION_LABEL: Channel Description
PODCAST_LINK_LABEL: Channel URL
PODCAST_LINK_HELPTEXT: Used in the RSS feed.
PODCAST_LANG_LABEL: Channel Language
PODCAST_LANG_HELPTEXT: Used only for the RSS feed.
PODCAST_COPYRIGHT_LABEL: Copyright
ITUNES_META:
TAB_TITLE: iTunes Meta
TAB_HELPTEXT: Metadata related to the podcast's iTunes section.
ITUNES_AUTHOR_LABEL: Author
ITUNES_AUTHOR_HELPTEXT: 'Example: "John Doe"'
ITUNES_OWNER_LABEL: Owner
ITUNES_OWNER_HELPTEXT: 'Example: "John Doe"'
ITUNES_OWNER_EMAIL_LABEL: Owner Email
ITUNES_OWNER_EMAIL_HELPTEXT: 'Example: "jdoe@example.com"'
ITUNES_IMAGE_LABEL: Channel Image
ITUNES_CATEGORY_LABEL: Category
ITUNES_SUBCATEGORY_LABEL: Sub-Category
ITUNES_EXPLICIT_LABEL: Explicit Content
ITUNES_EXPLICIT_HELPTEXT: Does this podcast contain content that might be inappropriate for children?
SERIES:
CONTENT:
TAB_TITLE: Podcast Series Content
PAGE_TITLE_LABEL: Series Title
SERIES_IMAGE_LABEL: Series Image
SERIES_IMAGE_HELPTEXT: Upload and save images below in the Page Media field. Then select one in this dropdown to act as the series main image.
EPISODE:
CONTENT:
TAB_TITLE: Podcast Episode
PAGE_TITLE_LABEL: Episode Title
SUBTITLE_LABEL: Episode Title
EPISODE_NUMBER_LABEL: Episode Number
PODCAST_AUDIO:
TAB_TITLE: Podcast Audio
LOCAL_HOSTED_LABEL: Locally Hosted Files
LOVAL_HOSTED_HELPTEXT: If you host your audio files on <strong>this</strong> server, upload the media on the Podcast tab.
EXTERNAL_HOSTED_LABEL: External Files
EXTERNAL_HOSTED_HELPTEXT: If you host your audio files on <strong>another</strong> server/CDN, fill in the field below
EXTERNAL_URL_LABEL: Remote Podcast URL
EXTERNAL_URL_HELPTEXT: Remote files will take precendence over locally hosted when viewed on a page.
ITUNES_META:
TAB_TITLE: iTunes Episode Meta
TAB_HELPTEXT: Metadata related to the podcast episode
AUTHOR_LABEL: Episode Author
AUTHOR_HELPTEXT: 'Example: "John Doe"'
IMAGE_LABEL: Episode Image
IMAGE_HELPTEXT: Upload and save images below in the Page Media field on the Podcast Episode tab. Then select one in this dropdown to act as the series main image.
EXPLICIT_LABEL: Explicit Content
EXPLICIT_HELPTEXT: Does this podcast contain content that might be inappropriate for children?

View File

@@ -75,30 +75,49 @@ class PodcastPlugin extends Plugin
$this->grav['assets'] $this->grav['assets']
->addCss('plugin://podcast/assets/css/podcast.css'); ->addCss('plugin://podcast/assets/css/podcast.css');
} }
/**
* Modifies the page header being saved to include getID3 metadata.
*/
public function onAdminSave($event) public function onAdminSave($event)
{ {
$obj = $event['object']; $obj = $event['object'];
// Process only podcast episodes page types. // Process only podcast episodes page types.
if (!($obj instanceof \Grav\Common\Page\Page) || $obj->template() != 'podcast-episode') { $obj_class = get_class($obj);
if (($obj_class != 'Grav\Common\Page\Page' && $obj_class != 'Grav\Common\Flex\Types\Pages\PageObject') || $obj->template() != 'podcast-episode') {
return; return;
} }
$header = $obj->header(); $header = $obj->header();
// Use local file for meta calculations, if present. // Use local file for meta calculations, if present.
// Else use remote file for meta, if present. // Else, use remote file for meta, if present.
// Else cleanup media entry in markdown header. // Else, cleanup media entry in markdown header.
if (isset($header->podcast['audio']['local'])) { if (isset($header->podcast['audio']['local']['select'])) {
// Create a temporary array to perform the array_shift on. $local['select'] = $header->podcast['audio']['local']['select'];
$local_audio = $header->podcast['audio']['local']; $media = $obj->media()->audios()[$local['select']];
$local_audio = array_shift($local_audio);
// Create array for backawards compatability with Grav content created with < v1.7.
$audio_meta['guid'] = $local_audio['path']; $audio = $header->podcast['audio'];
$audio_meta['type'] = $this->retreiveAudioType($audio_meta['guid']); $file_path = $media->relativePath();
$audio_meta['duration'] = $this->retreiveAudioDuration($audio_meta['guid']); //$file_url = $obj->getRoute()->getUri()->getPath() . DS . $local['select'];
$audio_meta['enclosure_length'] = filesize($audio_meta['guid']); $file_url = $obj->getRoute()->getRootPrefix();
$file_url .= DS . implode('/', $obj->getRoute()->getRouteParts());
$file_url .= DS . $local['select'];
$audio_meta['guid'] = $file_url;
$audio_meta['type'] = $this->retreiveAudioType($file_path);
$audio_meta['duration'] = $this->retreiveAudioDuration($file_path);
$audio_meta['enclosure_length'] = filesize($file_path);
$local_file = [
'name' => $local['select'],
'type' => $audio_meta['type'],
'size' => $audio_meta['enclosure_length'],
'path' => $file_url,
];
$local[$file_path] = $local_file;
$header->offsetSet('podcast.audio.local', $local);
} }
if (isset($header->podcast['audio']['remote']) && !isset($audio_meta)) { if (isset($header->podcast['audio']['remote']) && !isset($audio_meta)) {
// Download fle from external url to temporary location. // Download fle from external url to temporary location.
@@ -117,27 +136,24 @@ class PodcastPlugin extends Plugin
unset($header->podcast['audio']['meta']); unset($header->podcast['audio']['meta']);
} }
} }
// Reset the guid if using an external file source. // Reset the guid if using an external file source.
if (isset($header->podcast['audio']['remote']) && isset($header->podcast['audio']['meta'])) { if (isset($header->podcast['audio']['remote']) && isset($header->podcast['audio']['meta'])) {
$audio_meta['guid'] = $header->podcast['audio']['remote']; $audio_meta['guid'] = $header->podcast['audio']['remote'];
} }
// Prepare $obj to return new header data. // Prepare $obj to return new header data.
$new_header = $header->toArray();
if (isset($audio_meta)) { if (isset($audio_meta)) {
$header->podcast['audio']['meta'] = $audio_meta; $new_header['podcast']['audio']['meta'] = $audio_meta;
} else { } else {
// Cleanup any leftover data if neither local or remote file are set. // Cleanup any leftover data if neither local or remote file are set.
if (isset($header->podcast['audio']['meta'])) { unset($new_header['podcast']['audio']);
unset($header->podcast['audio']);
}
} }
$obj->header($header); $obj->header($new_header);
return $obj;
} }
/** /**
* Retrieve audio metadata filesize. * Retrieve audio metadata filesize.
* *
@@ -165,7 +181,7 @@ class PodcastPlugin extends Plugin
$id3 = GetID3Plugin::analyzeFile($file); $id3 = GetID3Plugin::analyzeFile($file);
return ($id3['mime_type']); return ($id3['mime_type']);
} }
/** /**
* Retrieve audio metadata duration. * Retrieve audio metadata duration.
* *
@@ -197,7 +213,7 @@ class PodcastPlugin extends Plugin
curl_exec($ch); curl_exec($ch);
$retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
// $retcode >= 400 -> not found; 200 -> found; 0 -> server not found. // $retcode >= 400 -> not found; 200 -> found; 0 -> server not found.
if ($retcode >= 400 || $retcode == 0) { if ($retcode >= 400 || $retcode == 0) {
$grav_messages = $this->grav['messages']; $grav_messages = $this->grav['messages'];
@@ -205,21 +221,21 @@ class PodcastPlugin extends Plugin
$grav_messages->add("Audio File metadata calculation failed!", 'error'); $grav_messages->add("Audio File metadata calculation failed!", 'error');
return null; return null;
} }
// Download file to temp location. // Download file to temp location.
if ($remote_file = fopen($url, 'rb')) { if ($remote_file = fopen($url, 'rb')) {
$local_file = tempnam('/tmp', 'podcast'); $local_file = tempnam('/tmp', 'podcast');
$handle = fopen($local_file, "w"); $handle = fopen($local_file, "w");
$contents = stream_get_contents($remote_file); $contents = stream_get_contents($remote_file);
fwrite($handle, $contents); fwrite($handle, $contents);
fclose($remote_file); fclose($remote_file);
fclose($handle); fclose($handle);
return $local_file; return $local_file;
} }
} }
/** /**
* Finds list of available iTunes categories. * Finds list of available iTunes categories.
* *

View File

@@ -1,2 +1 @@
enabled: true enabled: true
max_upload: 50

View File

@@ -20,7 +20,7 @@
<itunes:name>{{ channel.header.podcast.itunes.owner.name }}</itunes:name> <itunes:name>{{ channel.header.podcast.itunes.owner.name }}</itunes:name>
<itunes:email>{{ channel.header.podcast.itunes.owner.email }}</itunes:email> <itunes:email>{{ channel.header.podcast.itunes.owner.email }}</itunes:email>
</itunes:owner> </itunes:owner>
<itunes:image href="{{ base_url_absolute}}/{{ ((channel.header.podcast.itunes.image)|first.path) }}"/> <itunes:image href="{{ uri.base }}{{ channel.media[page.header.podcast.itunes.image].url(true) }}"/>
<itunes:category text="{{ channel.header.podcast.itunes.category }}"> <itunes:category text="{{ channel.header.podcast.itunes.category }}">
<itunes:category text="{{ channel.header.podcast.itunes.subcategory }}"/> <itunes:category text="{{ channel.header.podcast.itunes.subcategory }}"/>
</itunes:category> </itunes:category>
@@ -31,23 +31,23 @@
<item> <item>
<title>{{ episode.title }}</title> <title>{{ episode.title }}</title>
<link>{{ episode.url(true) }}</link> <link>{{ episode.url(true) }}</link>
{% if episode.header.podcast.episode_number %} {% if episode.header.podcast.episode_number -%}
<itunes:episode>{{ episode.header.podcast.episode_number }}</itunes:episode> <itunes:episode>{{ episode.header.podcast.episode_number }}</itunes:episode>
{% endif %} {% endif -%}
<itunes:author>{{ episode.header.podcast.itunes.author }}</itunes:author> <itunes:author>{{ episode.header.podcast.itunes.author }}</itunes:author>
<itunes:subtitle>{{ episdoe.header.podcast.subtitle }}</itunes:subtitle> <itunes:subtitle>{{ episode.header.podcast.itunes.subtitle }}</itunes:subtitle>
<itunes:summary>{{episode.content|striptags|truncate(120, true, " ", "&#x2026;")}}</itunes:summary> <itunes:summary>{{ episode.content|striptags|truncate(120, true, " ", "&#x2026;")}}</itunes:summary>
<itunes:image href="{{base_url_absolute}}/{{ (episode.header.podcast.itunes.image)|first.path|absolute_url }}"/> <itunes:image href="{{ uri.base }}/{{ episode.media[episode.header.podcast.itunes.image].url(true) }}"/>
{% if (episode.header.podcast.audio.remote) %} {% if ( episode.header.podcast.audio.remote ) -%}
<enclosure length="{{episode.header.podcast.audio.meta.enclosure_length}}" type="{{episode.header.podcast.audio.meta.type}}" url="{{episode.header.podcast.audio.remote}}"/> <enclosure length="{{ episode.header.podcast.audio.meta.enclosure_length }}" type="{{ episode.header.podcast.audio.meta.type }}" url="{{ episode.header.podcast.audio.remote }}"/>
<guid>{{episode.header.podcast.audio.remote}}</guid> <guid>{{ episode.header.podcast.audio.remote }}</guid>
{% else %} {% else -%}
<enclosure length="{{episode.header.podcast.audio.meta.enclosure_length}}" type="{{episode.header.podcast.audio.meta.type}}" url="{{base_url_absolute}}/{{episode.header.podcast.audio.meta.guid|absolute_url}}"/> <enclosure length="{{ episode.header.podcast.audio.meta.enclosure_length }}" type="{{ episode.header.podcast.audio.meta.type }}" url="{{ uri.base }}{{ episode.header.podcast.audio.meta.guid }}"/>
<guid>{{base_url_absolute}}/{{episode.header.podcast.audio.meta.guid|absolute_url}}</guid> <guid>{{ uri.base }}{{ episode.header.podcast.audio.meta.guid }}</guid>
{% endif %} {% endif -%}
<pubDate>{{ episode.header.publish_date ? episode.header.publish_date|date('r') : episode.date|date('r')}}</pubDate> <pubDate>{{ episode.header.publish_date ? episode.header.publish_date|date('r') : episode.date|date('r') }}</pubDate>
<itunes:duration>{{episode.header.podcast.audio.meta.duration}}</itunes:duration> <itunes:duration>{{ episode.header.podcast.audio.meta.duration }}</itunes:duration>
<itunes:explicit>{{episode.header.podcast.itunes.explicit}}</itunes:explicit> <itunes:explicit>{{ episode.header.podcast.itunes.explicit }}</itunes:explicit>
</item> </item>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -1,6 +1,6 @@
<div id = "episodes"> <div id = "episodes">
{% if (episodes|length > 0)%}
<ul> <ul>
{% for episode in episodes %} {% for episode in episodes %}
<li> <li>
<div class="episode-image"> <div class="episode-image">
@@ -19,10 +19,10 @@
{% endif %} {% endif %}
</h2> </h2>
</a> </a>
<p class = "episode-date"> <p class="episode-date">
{{ (episode.modified)|date('Y-m-d')|nicetime(false) }} {{ (episode.modified)|date('Y-m-d')|nicetime(false) }}
</p> </p>
<p class = "episode-description"> <p class="episode-description">
{% if episode.summary %} {% if episode.summary %}
{{ (episode.summary)|striptags|truncate(120) }} {{ (episode.summary)|striptags|truncate(120) }}
{% else %} {% else %}
@@ -31,16 +31,15 @@
</p> </p>
{% if episode.header.podcast.audio.meta %} {% if episode.header.podcast.audio.meta %}
<p class = "podcast-episode-audio"> <p class = "podcast-episode-audio">
{% if episode.header.podcast.audio.remote is null or episode.header.podcast.audio.remote is empty %} <audio controls="1" alt="episode.title"><source src="{{ episode.header.podcast.audio.meta.guid }}">Your browser does not support the audio tag.</audio>
{% set media_url = base_url ~ "/" ~ episode.header.podcast.audio.meta.guid %} <br/><a href ="{{ episode.header.podcast.audio.meta.guid }}">Download Audio</a>
{% else %}
{% set media_url = episode.header.podcast.audio.meta.guid %}
{% endif %}
<audio controls="1" alt="episode.title"><source src="{{media_url}}">Your browser does not support the audio tag.</audio>
<br/><a href ="{{media_url}}">Download Audio</a>
</p> </p>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %}
<p> No episodes </p>
{% endif %}
</div> </div>

View File

@@ -0,0 +1,12 @@
{% set mini_episodes = page.parent.children() %}
<div class="episodes-mini-list">
<h3>{{ 'PLUGIN_PODCAST.EPISODE_CONTENT.OTHER'|t|e }} '{{ page.parent.title }}'</h3>
<ul>
{% for e in mini_episodes|slice(0, 5) %}
<li><a href="{{ e.url() }}">{{ e.title }}</a></li>
{% endfor %}
{% if (mini_episodes|length > 1) %}
<li><a href="{{page.parent.url()}}">More >></a></li>
{% endif %}
</ul>
</div>

View File

@@ -5,7 +5,7 @@
<ul> <ul>
{% for s in series %} {% for s in series %}
<li> <li>
<a href="{{base_url_simple}}{{ s.route }}">{{ s.title }}</a> <a href="{{ base_url }}{{ s.route }}">{{ s.title }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -1,23 +1,46 @@
{% extends 'partials/base.html.twig' %} {% extends 'partials/base.html.twig' %}
{% block content %} {% block content %}
<h1>{{ header.title }}</h1>
{% if page.parent.name == 'podcast-series.md' %} {% if page.parent.template == 'podcast-channel' %}
<h2>An episode in the series {{page.parent.title}}</h2> {% set episodes = page.siblings %}
{% endif %} {% elseif page.template == 'podcast-series' %}
<h3>Posted {{ header.date|date('Y-m-d')|nicetime(false) }}</h3> {% set channel = page.parent %}
{{ page.content }} {% endif %}
{% if page.header.podcast.audio.meta %}
<p class = "podcast-episode-audio"> <div class="episode-container">
{% if page.header.podcast.audio.remote is null or page.header.podcast.audio.remote is empty %}
{% set media_url = base_url ~ "/" ~ page.header.podcast.audio.meta.guid %} <div class="episode-nav">
{% else %} {% include 'partials/breadcrumbs.html.twig'%}
{% set media_url = page.header.podcast.audio.meta.guid %} </div>
{% endif %}
<audio controls="1" alt="{{ episode.title }}"><source src="{{ media_url }}">Your browser does not support the audio tag.</audio> <div class="episode-flex-wrapper">
<br/><a href ="{{ media_url }}">Download Audio</a> <div class="episode-flex-child episode-top">
</p> <h1>{{ page.title }} {% if (header.podcast.audio.meta.guid) %}<small>({{ header.podcast.audio.meta.duration }})</small>{% endif %}</h1>
{% endif %} {% if (header.podcast.itunes.subtitle) -%}
<h2>{{ header.podcast.itunes.subtitle }}</h2
{% endif %}
<div class="episode-content-wrapper">
{% if (header.podcast.audio.meta.guid) %}
<div class="podcast-episode-audio">
<audio controls="1" alt="{{ episode.title }}"><source src="{{ page.header.podcast.audio.meta.guid }}">{{ 'PLUGIN_PODCAST.EPISODE_CONTENT.WARNING'|t|e }}</audio></br>
<a href ="{{ header.podcast.audio.meta.guid }}">{{ 'PLUGIN_PODCAST.EPISODE_CONTENT.DOWNLOAD'|t|e }}</a> | Posted {{ header.date|date('Y-m-d')|nicetime(false) }}
</div>
{% else %}
<p>No audio for this episode</p>
{% endif %}
</div>
<div class="episode-flex-child episode-content">
<h3>{{ 'PLUGIN_PODCAST.EPISODE_CONTENT.HEADER'|t|e }}</h3>
{{ page.content|raw }}
</div>
<div class="episode-flex-child">
{% include 'partials/podcast_episodes_mini_list.html.twig'%}
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -3,15 +3,12 @@
{% block content %} {% block content %}
{% set series = page.parent.collection({ 'items': '@self.descendants', 'order': {'by': 'date', 'dir': 'desc'}} ).ofType('podcast-series') %} {% set series = page.parent.collection({ 'items': '@self.descendants', 'order': {'by': 'date', 'dir': 'desc'}} ).ofType('podcast-series') %}
{% set episodes = page.collection({ 'items': '@self.descendants', 'order': {'by': 'date', 'dir': 'desc'}} ).ofType('podcast-episode') %} {% set episodes = page.collection({ 'items': '@self.descendants', 'order': {'by': 'date', 'dir': 'desc'}} ).ofType('podcast-episode') %}
<div class="podcast-channel-header"> <div class="podcast-channel-header">
<div class = "podcast-channel-image"> <div class = "podcast-channel-image">
{% if not (page.header.series.image) %} {% if not (header.series.image) and (page.parent.header.podcast.itunes.image)%}
{% set featured_image = ((page.parent.header.podcast.itunes.image)|first) %} {{ page.parent.media[page.parent.header.podcast.itunes.image].resize(200, 200).html()|raw }}
{{ page.parent.media[featured_image.name].resize(200, 200).html() }}
{% else %} {% else %}
{% set featured_image = ((page.header.series.image)|first) %} {{ page.media[header.series.image].resize(200, 200).html()|raw }}
{{ page.media[featured_image.name].resize(200, 200).html() }}
{% endif %} {% endif %}
</div> </div>
@@ -21,9 +18,9 @@
{% if not page.content %} {% if not page.content %}
{% set description = page.parent.header.podcast.description %} {% set description = page.parent.header.podcast.description %}
{% else %} {% else %}
{% set description = page.content %} {% set description = page.content() %}
{% endif %} {% endif %}
<p class = "description">{{ description }}</p> <p class = "description">{{ description|raw }}</p>
</div> </div>
<div class="channel-links"> <div class="channel-links">
<a href="{{ base_url }}{{ page.route }}.rss"><i class="fa fa-rss" aria-hidden="true"></i>{{ 'PLUGIN_PODCAST.SERIES'|t }} | {{ page.title }}</a> <a href="{{ base_url }}{{ page.route }}.rss"><i class="fa fa-rss" aria-hidden="true"></i>{{ 'PLUGIN_PODCAST.SERIES'|t }} | {{ page.title }}</a>