From 39401222feca348dd3ff80e51cf1544c60c2633b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Jan 2022 18:25:13 +0000 Subject: [PATCH] Podcast episode screen --- daemon.py | 29 ++++ epicyon-podcast.css | 382 +++++++++++++++++++++++++++++++++++++++++ newswire.py | 24 +++ tests.py | 2 + webapp_column_right.py | 3 +- webapp_podcast.py | 202 ++++++++++++++++++++++ 6 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 epicyon-podcast.css create mode 100644 webapp_podcast.py diff --git a/daemon.py b/daemon.py index b0d8128f6..651176e8e 100644 --- a/daemon.py +++ b/daemon.py @@ -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: diff --git a/epicyon-podcast.css b/epicyon-podcast.css new file mode 100644 index 000000000..5510098c3 --- /dev/null +++ b/epicyon-podcast.css @@ -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; + } +} diff --git a/newswire.py b/newswire.py index c14c64737..dfab73216 100644 --- a/newswire.py +++ b/newswire.py @@ -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 = ['' 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 '' not in rss_item: continue + title = rss_item.split('')[1] title = _remove_cdata(title.split('')[0]) title = remove_html(title) + description = '' if '' in rss_item and '' in rss_item: description = rss_item.split('')[1] @@ -500,11 +522,13 @@ def _xml2str_to_dict(base_dir: str, domain: str, xml_str: str, description = rss_item.split('')[1] description = description.split('')[0] description = remove_html(description) + link = rss_item.split('')[1] link = link.split('')[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): diff --git a/tests.py b/tests.py index cbe943bf1..d3fc26f28 100644 --- a/tests.py +++ b/tests.py @@ -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 diff --git a/webapp_column_right.py b/webapp_column_right.py index 62df82d36..746958e0e 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -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 diff --git a/webapp_podcast.py b/webapp_podcast.py new file mode 100644 index 000000000..83ef89bb2 --- /dev/null +++ b/webapp_podcast.py @@ -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 = '
\n' + podcast_str += '
\n' + podcast_str += '
    \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 += ', ' + performer['group'] + '' + 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 += '
  • \n' + podcast_str += '
    \n' + podcast_str += ' \n' + podcast_str += \ + ' \n' + podcast_str += \ + '
    ' + performer_title + '
    \n' + podcast_str += '
    \n' + podcast_str += '
    \n' + podcast_str += '
  • \n' + + podcast_str += '
\n' + podcast_str += '
\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 = '
\n' + podcast_str += '
\n' + podcast_str += '
    \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 += '
  • \n' + preview_url = \ + link_url + '#t=' + performer['startTime'] + ',' + end_time + soundbite_title = translate['Preview'] + if ctr > 0: + soundbite_title += ' ' + str(ctr) + podcast_str += \ + ' \n' + podcast_str += '
  • \n' + ctr += 1 + + podcast_str += '
\n' + podcast_str += '
\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 += '

\n' + podcast_str += '
\n' + podcast_str += '
\n' + podcast_str += '
\n' + podcast_str += ' \n' + if image_src == 'srcset': + podcast_str += ' \n' + else: + podcast_str += ' \n' + podcast_str += '
\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 += \ + '\n' + + podcast_title = remove_html(newswire_item[0]) + if podcast_title: + podcast_str += \ + '

\n' + if newswire_item[4]: + podcast_description = remove_html(newswire_item[4]) + if podcast_description: + podcast_str += '

' + podcast_description + '

\n' + + # donate button + if podcast_properties.get('funding'): + if podcast_properties['funding'].get('url'): + donate_url = podcast_properties['funding']['url'] + podcast_str += \ + '

\n' + + podcast_str += _html_podcast_performers(podcast_properties) + + podcast_str += ' \n' + podcast_str += '
\n' + + podcast_str += html_footer() + return podcast_str