Podcast episode screen

merge-requests/30/head
Bob Mottram 2022-01-11 18:25:13 +00:00
parent 3524117e9c
commit 39401222fe
6 changed files with 641 additions and 1 deletions

View File

@ -156,6 +156,7 @@ from blog import html_blog_page
from blog import html_blog_post
from blog import html_edit_blog
from blog import get_blog_address
from webapp_podcast import html_podcast_episode
from webapp_theme_designer import html_theme_designer
from webapp_minimalbutton import set_minimal
from webapp_minimalbutton import is_minimal
@ -14062,6 +14063,34 @@ class PubServer(BaseHTTPRequestHandler):
self._write(msg)
return
# show a podcast episode
if authorized and users_in_path and html_getreq and \
'?podepisode=' in self.path:
nickname = self.path.split('/users/')[1]
if '/' in nickname:
nickname = nickname.split('/')[0]
episode_timestamp = self.path.split('?podepisode=')[1]
if self.server.newswire.get(episode_timestamp):
pod_episode = self.server.newswire[episode_timestamp]
html_str = \
html_podcast_episode(self.server.css_cache,
self.server.translate,
self.server.base_dir,
nickname,
self.server.domain,
pod_episode,
self.server.themeName,
self.server.default_timeline,
self.server.text_mode_banner,
self.server.access_keys)
if html_str:
msg = html_str.encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
None, calling_domain, False)
self._write(msg)
return
# redirect to the welcome screen
if html_getreq and authorized and users_in_path and \
'/welcome' not in self.path:

382
epicyon-podcast.css 100644
View File

@ -0,0 +1,382 @@
@charset "UTF-8";
:root {
--avatar-rounding: 10%;
--options-bg-color: #282c37;
--options-link-bg-color: transparent;
--options-fg-color: #dddddd;
--options-main-link-color: #999;
--options-main-visited-color: #888;
--border-color: #505050;
--font-size-header: 18px;
--font-color-header: #ccc;
--font-size: 40px;
--font-size2: 24px;
--font-size3: 38px;
--font-size4: 22px;
--font-size5: 20px;
--text-entry-foreground: #ccc;
--text-entry-background: #111;
--time-color: #aaa;
--button-text: #FFFFFF;
--button-small-text: #FFFFFF;
--button-background-hover: #777;
--button-background: #999;
--button-small-background: #999;
--hashtag-margin: 2%;
--hashtag-vertical-spacing1: 50px;
--hashtag-vertical-spacing2: 100px;
--hashtag-vertical-spacing3: 100px;
--hashtag-vertical-spacing4: 150px;
--hashtag-size1: 30px;
--hashtag-size2: 40px;
--follow-text-size1: 24px;
--follow-text-size2: 40px;
--follow-text-entry-width: 90%;
--focus-color: white;
--petname-width-chars: 16ch;
--options-main-link-color-hover: #bbb;
--rendering: normal;
}
@font-face {
font-family: 'Bedstead';
font-style: italic;
font-weight: normal;
font-display: block;
src: url('./fonts/bedstead.otf') format('opentype');
}
@font-face {
font-family: 'Bedstead';
font-style: normal;
font-weight: normal;
font-display: block;
src: url('./fonts/bedstead.otf') format('opentype');
}
body, html {
background-image: url("podcast-background.jpg");
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-color: var(--options-bg-color);
color: var(--options-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
max-width: 100%;
min-width: 600px;
image-rendering: var(--rendering);
}
a, u {
color: var(--options-fg-color);
}
a:visited{
color: var(--options-main-visited-color);
background: var(--options-link-bg-color);
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--options-main-link-color);
background: var(--options-link-bg-color);
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--options-main-link-color-hover);
}
a:visited:hover {
color: var(--options-main-link-color-hover);
}
a:focus {
border: 2px solid var(--focus-color);
}
.transparent {
color: transparent;
background: transparent;
font-size: 0px;
line-height: 0px;
height: 0px;
}
.follow {
height: 100%;
position: relative;
background-color: var(--options-bg-color);
}
.followAvatar {
margin: 0% 0;
}
.followAvatar img {
border-radius: 10%;
width: 20%;
min-width: 200px;
}
.imText {
font-size: var(--font-size4);
color: var(--options-main-link-color);
}
.pgp {
font-size: var(--font-size5);
color: var(--options-main-link-color);
background: var(--options-link-bg-color);
}
.button:hover {
background-color: var(--button-background-hover);
}
.options {
font-size: var(--font-size);
}
.options img {
border-radius: var(--avatar-rounding);
background-color: var(--options-bg-color);
width: 15%;
}
@media screen and (min-width: 400px) {
textarea {
font-family: Arial, Helvetica, sans-serif;
font-size: var(--font-size4);
width: 90%;
background-color: var(--text-entry-background);
color: var(--text-entry-foreground);
}
.followText {
font-size: var(--follow-text-size1);
}
input[type=text] {
width: var(--follow-text-entry-width);
clear: both;
max-width: 30%;
min-width: var(--petname-width-chars);
font-size: 24px;
text-align: center;
color: var(--text-entry-foreground);
background-color: var(--text-entry-background);
font-family: Arial, Helvetica, sans-serif;
}
.button {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 10px;
font-size: 24px;
width: 10ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
.buttonIcon {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 10px 65px;
font-size: 24px;
max-width: 200px;
min-width: 100px;
cursor: pointer;
}
.buttonsmall {
border-radius: 4px;
background-color: var(--button-small-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-small-text);
text-align: center;
padding: 10px;
font-size: 24px;
width: 7ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
input[type=checkbox]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 20px 30px;
}
}
@media screen and (max-width: 1000px) {
textarea {
font-family: Arial, Helvetica, sans-serif;
font-size: var(--font-size);
width: 90%;
background-color: var(--text-entry-background);
color: var(--text-entry-foreground);
}
.followText {
font-size: var(--follow-text-size2);
}
input[type=text] {
width: var(--follow-text-entry-width);
clear: both;
font-size: var(--font-size);
text-align: center;
max-width: 50%;
min-width: var(--petname-width-chars);
color: var(--text-entry-foreground);
background-color: var(--text-entry-background);
font-family: Arial, Helvetica, sans-serif;
}
.button {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 10px;
font-size: var(--font-size);
width: 10ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
.buttonIcon {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 6px 80px;
font-size: var(--font-size);
max-width: 200px;
min-width: 100px;
cursor: pointer;
}
.buttonsmall {
border-radius: 4px;
background-color: var(--button-small-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-small-text);
text-align: center;
padding: 10px;
font-size: var(--font-size);
width: 7ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
input[type=checkbox]
{
-ms-transform: scale(4);
-moz-transform: scale(4);
-webkit-transform: scale(4);
-o-transform: scale(4);
transform: scale(4);
padding: 20px;
margin: 30px 40px;
}
}
@media screen and (max-width: 480px) {
textarea {
font-family: Arial, Helvetica, sans-serif;
font-size: var(--font-size2);
width: 90%;
background-color: var(--text-entry-background);
color: var(--text-entry-foreground);
}
.followText {
font-size: var(--follow-text-size2);
}
input[type=text] {
width: var(--follow-text-entry-width);
clear: both;
font-size: var(--font-size2);
text-align: center;
max-width: 50%;
min-width: var(--petname-width-chars);
color: var(--text-entry-foreground);
background-color: var(--text-entry-background);
font-family: Arial, Helvetica, sans-serif;
}
.button {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 10px;
font-size: var(--font-size2);
width: 10ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
.buttonIcon {
border-radius: 4px;
background-color: var(--button-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-text);
text-align: center;
padding: 6px 80px;
font-size: var(--font-size2);
max-width: 200px;
min-width: 100px;
cursor: pointer;
}
.buttonsmall {
border-radius: 4px;
background-color: var(--button-small-background);
font-family: Arial, Helvetica, sans-serif;
border: none;
color: var(--button-small-text);
text-align: center;
padding: 10px;
font-size: var(--font-size2);
width: 7ch;
max-width: 200px;
min-width: 100px;
cursor: pointer;
margin: 30px;
}
input[type=checkbox]
{
-ms-transform: scale(4);
-moz-transform: scale(4);
-webkit-transform: scale(4);
-o-transform: scale(4);
transform: scale(4);
padding: 20px;
margin: 30px 40px;
}
}

View File

@ -445,6 +445,26 @@ def xml_podcast_to_dict(xml_str: str) -> {}:
podcast_properties[pod_key] = pod_entry
ctr += 1
# get the image for the podcast, if it exists
episode_image_tags = ['<itunes:image']
for image_tag in episode_image_tags:
if image_tag not in xml_str:
continue
episode_image = xml_str.split(image_tag)[1]
if 'href="' in episode_image:
episode_image = episode_image.split('href="')[1]
if '"' in episode_image:
episode_image = episode_image.split('"')[0]
podcast_properties['image'] = episode_image
break
else:
if '>' in episode_image:
episode_image = episode_image.split('>')[1]
if '<' in episode_image:
episode_image = episode_image.split('<')[0]
if '://' in episode_image and '.' in episode_image:
podcast_properties['image'] = episode_image
break
return podcast_properties
@ -487,9 +507,11 @@ def _xml2str_to_dict(base_dir: str, domain: str, xml_str: str,
continue
if '</pubDate>' not in rss_item:
continue
title = rss_item.split('<title>')[1]
title = _remove_cdata(title.split('</title>')[0])
title = remove_html(title)
description = ''
if '<description>' in rss_item and '</description>' in rss_item:
description = rss_item.split('<description>')[1]
@ -500,11 +522,13 @@ def _xml2str_to_dict(base_dir: str, domain: str, xml_str: str,
description = rss_item.split('<media:description>')[1]
description = description.split('</media:description>')[0]
description = remove_html(description)
link = rss_item.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
continue
item_domain = link.split('://')[1]
if '/' in item_domain:
item_domain = item_domain.split('/')[0]
if is_blocked_domain(base_dir, item_domain):

View File

@ -6439,6 +6439,8 @@ def _test_xml_podcast_dict() -> None:
assert podcast_properties.get('funding')
assert int(podcast_properties['episode']) == 5
assert podcast_properties['funding']['text'] == "Support the show"
assert podcast_properties['funding']['url'] == \
"https://whoframed.rodger/donate"
assert len(podcast_properties['transcripts']) == 3
assert len(podcast_properties['valueRecipients']) == 2
assert len(podcast_properties['persons']) == 5

View File

@ -269,7 +269,8 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool,
if len(item) > 8:
# change the link url to a podcast episode screen
podcast_properties = item[8]
if podcast_properties.get('persons'):
if podcast_properties.get('persons') and \
podcast_properties.get('image'):
link_url = '/users/' + nickname + '/?podepisode=' + date_str
html_str += separator_str

202
webapp_podcast.py 100644
View File

@ -0,0 +1,202 @@
__filename__ = "webapp_podcast.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface Columns"
import os
from shutil import copyfile
from utils import get_config_param
from utils import remove_html
from media import path_is_audio
from webapp_utils import get_broken_link_substitute
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
from webapp_utils import html_keyboard_navigation
def _html_podcast_performers(podcast_properties: {}) -> str:
"""Returns html for performers of a podcast
"""
if not podcast_properties.get('persons'):
return ''
# list of performers
podcast_str = '<div class="performers">\n'
podcast_str += ' <center>\n'
podcast_str += '<ul>\n'
for performer in podcast_properties['persons']:
if not performer.get('text'):
continue
performer_name = performer['text']
performer_title = performer_name
if performer.get('role'):
performer_title += ' (' + performer['role'] + ')'
if performer.get('group'):
performer_title += ', <i>' + performer['group'] + '</i>'
performer_title = remove_html(performer_title)
performer_url = ''
if performer.get('href'):
performer_url = performer['href']
performer_img = ''
if performer.get('img'):
performer_img = performer['img']
podcast_str += ' <li>\n'
podcast_str += ' <figure>\n'
podcast_str += ' <a href="' + performer_url + '">\n'
podcast_str += \
' <img loading="lazy" src="' + performer_img + \
'" alt="" />\n'
podcast_str += \
' <figcaption>' + performer_title + '</figcaption>\n'
podcast_str += ' </a>\n'
podcast_str += ' </figure>\n'
podcast_str += ' </li>\n'
podcast_str += '</ul>\n'
podcast_str += '</div>\n'
return podcast_str
def _html_podcast_soundbites(link_url: str, extension: str,
podcast_properties: {},
translate: {}) -> str:
"""Returns html for podcast soundbites
"""
if not podcast_properties.get('soundbites'):
return ''
podcast_str = '<div class="performers">\n'
podcast_str += ' <center>\n'
podcast_str += '<ul>\n'
ctr = 1
for performer in podcast_properties['soundbites']:
if not performer.get('startTime'):
continue
if not performer['startTime'].isdigit():
continue
if not performer.get('duration'):
continue
if not performer['duration'].isdigit():
continue
end_time = str(float(performer['startTime']) +
float(performer['duration']))
podcast_str += ' <li>\n'
preview_url = \
link_url + '#t=' + performer['startTime'] + ',' + end_time
soundbite_title = translate['Preview']
if ctr > 0:
soundbite_title += ' ' + str(ctr)
podcast_str += \
' <audio controls>\n' + \
' <p>' + soundbite_title + '</p>\n' + \
' <source src="' + preview_url + '" type="audio/' + \
extension.replace('.', '') + '">' + \
translate['Your browser does not support the audio element.'] + \
'</audio>\n'
podcast_str += ' </li>\n'
ctr += 1
podcast_str += '</ul>\n'
podcast_str += '</div>\n'
return podcast_str
def html_podcast_episode(css_cache: {}, translate: {},
base_dir: str, nickname: str, domain: str,
newswire_item: [], theme: str,
default_timeline: str,
text_mode_banner: str, access_keys: {}) -> str:
"""Returns html for a podcast episode, giebn an item from the newswire
"""
css_filename = base_dir + '/epicyon-podcast.css'
if os.path.isfile(base_dir + '/podcast.css'):
css_filename = base_dir + '/podcast.css'
if os.path.isfile(base_dir + '/accounts/podcast-background-custom.jpg'):
if not os.path.isfile(base_dir + '/accounts/podcast-background.jpg'):
copyfile(base_dir + '/accounts/podcast-background.jpg',
base_dir + '/accounts/podcast-background.jpg')
instance_title = get_config_param(base_dir, 'instanceTitle')
podcast_str = \
html_header_with_external_style(css_filename, instance_title, None)
podcast_properties = newswire_item[8]
image_url = ''
image_src = 'src'
if podcast_properties.get('images'):
if podcast_properties['images'].get('srcset'):
image_url = podcast_properties['images']['srcset']
image_src = 'srcset'
if not image_url and podcast_properties.get('image'):
image_url = podcast_properties['image']
link_url = newswire_item[1]
podcast_str += html_keyboard_navigation(text_mode_banner, {}, {})
podcast_str += '<br><br>\n'
podcast_str += '<div class="options">\n'
podcast_str += ' <div class="optionsAvatar">\n'
podcast_str += ' <center>\n'
podcast_str += ' <a href="' + link_url + '">\n'
if image_src == 'srcset':
podcast_str += ' <img loading="lazy" srcset="' + image_url + \
'" alt="" ' + get_broken_link_substitute() + '/></a>\n'
else:
podcast_str += ' <img loading="lazy" src="' + image_url + \
'" alt="" ' + get_broken_link_substitute() + '/></a>\n'
podcast_str += ' </div>\n'
if path_is_audio(link_url):
if '.mp3' in link_url:
extension = 'mp3'
else:
extension = 'ogg'
podcast_str += _html_podcast_soundbites(link_url, extension,
podcast_properties,
translate)
# podcast player widget
podcast_str += \
'<audio controls>\n' + \
'<source src="' + link_url + '" type="audio/' + \
extension.replace('.', '') + '">' + \
translate['Your browser does not support the audio element.'] + \
'</audio>\n'
podcast_title = remove_html(newswire_item[0])
if podcast_title:
podcast_str += \
'<p><label class="podcast-title">"' + podcast_title + \
'</label></p>\n'
if newswire_item[4]:
podcast_description = remove_html(newswire_item[4])
if podcast_description:
podcast_str += '<p>' + podcast_description + '</p>\n'
# donate button
if podcast_properties.get('funding'):
if podcast_properties['funding'].get('url'):
donate_url = podcast_properties['funding']['url']
podcast_str += \
'<p><a href="' + donate_url + \
'"><button class="donateButton">' + translate['Donate'] + \
'</button></a></p>\n'
podcast_str += _html_podcast_performers(podcast_properties)
podcast_str += ' </center>\n'
podcast_str += '</div>\n'
podcast_str += html_footer()
return podcast_str