diff --git a/blocking.py b/blocking.py index 045f6c70b..19ee64470 100644 --- a/blocking.py +++ b/blocking.py @@ -290,7 +290,7 @@ def update_blocked_cache(base_dir: str, with open(global_blocking_filename, 'r') as fp_blocked: blocked_lines = fp_blocked.readlines() # remove newlines - for index in range(len(blocked_lines)): + for index, _ in enumerate(blocked_lines): blocked_lines[index] = blocked_lines[index].replace('\n', '') # update the cache blocked_cache.clear() diff --git a/code-of-conduct.md b/code-of-conduct.md index 83ed3d4d1..5dc766847 100644 --- a/code-of-conduct.md +++ b/code-of-conduct.md @@ -38,7 +38,7 @@ No insults, harassment (sexual or otherwise), condescension, ad hominem, threats Condescension means treating others as inferior. Subtle condescension still violates the Code of Conduct even if not blatantly demeaning. -No stereotyping of or promoting prejudice or discrimination against particular groups or classes/castes of people, including sexism, racism, homophobia, transphobia, age discrimination or discrimination based upon nationality. +No stereotyping of or promoting prejudice or discrimination against particular groups or classes/castes of people, including sexism, racism, homophobia, transphobia, denying people their right to join or create a trade union, age discrimination or discrimination based upon nationality. In cases where criticism of ideology or culture remains on-topic, respectfully discuss the ideas. diff --git a/daemon.py b/daemon.py index b0d8128f6..9fbfa2984 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,36 @@ 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].strip() + episode_timestamp = episode_timestamp.replace('__', ' ') + episode_timestamp = episode_timestamp.replace('aa', ':') + 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.theme_name, + 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/newsdaemon.py b/newsdaemon.py index cf1b21aeb..cddb460d4 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -538,21 +538,21 @@ def _create_news_mirror(base_dir: str, domain: str, return True -def _convert_rs_sto_activity_pub(base_dir: str, http_prefix: str, - domain: str, port: int, - newswire: {}, - translate: {}, - recent_posts_cache: {}, - max_recent_posts: int, - session, cached_webfingers: {}, - person_cache: {}, - federation_list: [], - send_threads: [], post_log: [], - max_mirrored_articles: int, - allow_local_network_access: bool, - system_language: str, - low_bandwidth: bool, - content_license_url: str) -> None: +def _convert_rss_to_activitypub(base_dir: str, http_prefix: str, + domain: str, port: int, + newswire: {}, + translate: {}, + recent_posts_cache: {}, + max_recent_posts: int, + session, cached_webfingers: {}, + person_cache: {}, + federation_list: [], + send_threads: [], post_log: [], + max_mirrored_articles: int, + allow_local_network_access: bool, + system_language: str, + low_bandwidth: bool, + content_license_url: str) -> None: """Converts rss items in a newswire into posts """ if not newswire: @@ -627,6 +627,10 @@ def _convert_rs_sto_activity_pub(base_dir: str, http_prefix: str, '
' + \ translate['Read more...'] + '' +# podcast_properties = None +# if len(item) > 8: +# podcast_properties = item[8] + followers_only = False # NOTE: the id when the post is created will not be # consistent (it's based on the current time, not the @@ -751,7 +755,7 @@ def _convert_rs_sto_activity_pub(base_dir: str, http_prefix: str, try: os.remove(filename + '.arrived') except OSError: - print('EX: _convert_rs_sto_activity_pub ' + + print('EX: _convert_rss_to_activitypub ' + 'unable to delete ' + filename + '.arrived') # setting the url here links to the activitypub object @@ -829,22 +833,22 @@ def run_newswire_daemon(base_dir: str, httpd, print('No new newswire') print('Converting newswire to activitypub format') - _convert_rs_sto_activity_pub(base_dir, - http_prefix, domain, port, - new_newswire, translate, - httpd.recent_posts_cache, - httpd.max_recent_posts, - httpd.session, - httpd.cached_webfingers, - httpd.person_cache, - httpd.federation_list, - httpd.send_threads, - httpd.postLog, - httpd.max_mirrored_articles, - httpd.allow_local_network_access, - httpd.system_language, - httpd.low_bandwidth, - httpd.content_license_url) + _convert_rss_to_activitypub(base_dir, + http_prefix, domain, port, + new_newswire, translate, + httpd.recent_posts_cache, + httpd.max_recent_posts, + httpd.session, + httpd.cached_webfingers, + httpd.person_cache, + httpd.federation_list, + httpd.send_threads, + httpd.postLog, + httpd.max_mirrored_articles, + httpd.allow_local_network_access, + httpd.system_language, + httpd.low_bandwidth, + httpd.content_license_url) print('Newswire feed converted to ActivityPub') if httpd.max_news_posts > 0: diff --git a/newswire.py b/newswire.py index 4e261897f..c63dd710d 100644 --- a/newswire.py +++ b/newswire.py @@ -203,7 +203,8 @@ def _add_newswire_dict_entry(base_dir: str, domain: str, description: str, moderated: bool, mirrored: bool, tags: [], - max_tags: int, session, debug: bool) -> None: + max_tags: int, session, debug: bool, + podcast_properties: {}) -> None: """Update the newswire dictionary """ # remove any markup @@ -246,7 +247,8 @@ def _add_newswire_dict_entry(base_dir: str, domain: str, description, moderated, post_tags, - mirrored + mirrored, + podcast_properties ] @@ -382,6 +384,138 @@ def _xml2str_to_hashtag_categories(base_dir: str, xml_str: str, False, force) +def xml_podcast_to_dict(xml_str: str) -> {}: + """podcasting extensions for RSS feeds + See https://github.com/Podcastindex-org/podcast-namespace/ + blob/main/docs/1.0.md + """ + if '' not in pod_line: + ctr += 1 + continue + if ' ' not in pod_line.split('>')[0]: + pod_key = pod_line.split('>')[0].strip() + pod_val = pod_line.split('>', 1)[1].strip() + if '<' in pod_val: + pod_val = pod_val.split('<')[0] + podcast_properties[pod_key] = pod_val + ctr += 1 + continue + pod_key = pod_line.split(' ')[0] + + pod_fields = ( + 'url', 'geo', 'osm', 'type', 'method', 'group', + 'owner', 'srcset', 'img', 'role', 'address', 'suggested', + 'startTime', 'duration', 'href', 'name', 'pubdate', + 'length', 'season', 'email' + ) + pod_entry = {} + for pod_field in pod_fields: + if pod_field + '="' not in pod_line: + continue + pod_str = pod_line.split(pod_field + '="')[1] + if '"' not in pod_str: + continue + pod_val = pod_str.split('"')[0] + pod_entry[pod_field] = pod_val + + pod_text = pod_line.split('>')[1] + if '<' in pod_text: + pod_text = pod_text.split('<')[0].strip() + if pod_text: + pod_entry['text'] = pod_text + + if pod_key + 's' in podcast_properties: + if isinstance(podcast_properties[pod_key + 's'], list): + podcast_properties[pod_key + 's'].append(pod_entry) + else: + podcast_properties[pod_key] = pod_entry + else: + podcast_properties[pod_key] = pod_entry + ctr += 1 + + # get the image for the podcast, if it exists + podcast_episode_image = None + 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_episode_image = episode_image + break + + if podcast_episode_image: + podcast_properties['image'] = podcast_episode_image + + if 'Y' in xml_str or \ + 'T' in xml_str or \ + '1' in xml_str: + podcast_properties['explicit'] = True + else: + podcast_properties['explicit'] = False + else: + if ' (str, str): + """Extracts rss link from rss item string + """ + mime_type = None + + if '' in enclosure: + enclosure = enclosure.split('>')[0] + if ' type="' in enclosure: + mime_type = enclosure.split(' type="')[1] + if '"' in mime_type: + mime_type = mime_type.split('"')[0] + if 'url="' in enclosure and \ + ('"audio/' in enclosure or '"video/' in enclosure): + link_str = enclosure.split('url="')[1] + if '"' in link_str: + link = link_str.split('"')[0] + if '://' in link: + return link, mime_type + + link = rss_item.split('')[1] + link = link.split('')[0] + if '://' not in link: + return None, None + return link, mime_type + + def _xml2str_to_dict(base_dir: str, domain: str, xml_str: str, moderated: bool, mirrored: bool, max_posts_per_source: int, @@ -421,9 +555,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] @@ -434,13 +570,15 @@ 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: + + link, link_mime_type = get_link_from_rss_item(rss_item) + if not 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): continue pub_date = rss_item.split('')[1] @@ -451,18 +589,21 @@ def _xml2str_to_dict(base_dir: str, domain: str, xml_str: str, if _valid_feed_date(pub_date_str): post_filename = '' votes_status = [] + podcast_properties = xml_podcast_to_dict(rss_item) + if podcast_properties: + podcast_properties['linkMimeType'] = link_mime_type _add_newswire_dict_entry(base_dir, domain, result, pub_date_str, title, link, votes_status, post_filename, description, moderated, - mirrored, [], 32, session, debug) + mirrored, [], 32, session, debug, + podcast_properties) post_ctr += 1 if post_ctr >= max_posts_per_source: break if post_ctr > 0: - print('Added ' + str(post_ctr) + - ' rss 2.0 feed items to newswire') + print('Added ' + str(post_ctr) + ' rss 2.0 feed items to newswire') return result @@ -522,13 +663,15 @@ def _xml1str_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: + + link, link_mime_type = get_link_from_rss_item(rss_item) + if not 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): continue pub_date = rss_item.split('')[1] @@ -539,18 +682,21 @@ def _xml1str_to_dict(base_dir: str, domain: str, xml_str: str, if _valid_feed_date(pub_date_str): post_filename = '' votes_status = [] + podcast_properties = xml_podcast_to_dict(rss_item) + if podcast_properties: + podcast_properties['linkMimeType'] = link_mime_type _add_newswire_dict_entry(base_dir, domain, result, pub_date_str, title, link, votes_status, post_filename, description, moderated, - mirrored, [], 32, session, debug) + mirrored, [], 32, session, debug, + podcast_properties) post_ctr += 1 if post_ctr >= max_posts_per_source: break if post_ctr > 0: - print('Added ' + str(post_ctr) + - ' rss 1.0 feed items to newswire') + print('Added ' + str(post_ctr) + ' rss 1.0 feed items to newswire') return result @@ -598,13 +744,15 @@ def _atom_feed_to_dict(base_dir: str, domain: str, xml_str: str, description = atom_item.split('')[1] description = description.split('')[0] description = remove_html(description) - link = atom_item.split('')[1] - link = link.split('')[0] - if '://' not in link: + + link, link_mime_type = get_link_from_rss_item(atom_item) + if not 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): continue pub_date = atom_item.split('')[1] @@ -615,18 +763,21 @@ def _atom_feed_to_dict(base_dir: str, domain: str, xml_str: str, if _valid_feed_date(pub_date_str): post_filename = '' votes_status = [] + podcast_properties = xml_podcast_to_dict(atom_item) + if podcast_properties: + podcast_properties['linkMimeType'] = link_mime_type _add_newswire_dict_entry(base_dir, domain, result, pub_date_str, title, link, votes_status, post_filename, description, moderated, - mirrored, [], 32, session, debug) + mirrored, [], 32, session, debug, + podcast_properties) post_ctr += 1 if post_ctr >= max_posts_per_source: break if post_ctr > 0: - print('Added ' + str(post_ctr) + - ' atom feed items to newswire') + print('Added ' + str(post_ctr) + ' atom feed items to newswire') return result @@ -732,7 +883,8 @@ def _json_feed_v1to_dict(base_dir: str, domain: str, xml_str: str, title, link, votes_status, post_filename, description, moderated, - mirrored, [], 32, session, debug) + mirrored, [], 32, session, debug, + None) post_ctr += 1 if post_ctr >= max_posts_per_source: break @@ -805,7 +957,8 @@ def _atom_feed_yt_to_dict(base_dir: str, domain: str, xml_str: str, title, link, votes_status, post_filename, description, moderated, mirrored, - [], 32, session, debug) + [], 32, session, debug, + None) post_ctr += 1 if post_ctr >= max_posts_per_source: break @@ -829,24 +982,24 @@ def _xml_str_to_dict(base_dir: str, domain: str, xml_str: str, max_posts_per_source, max_feed_item_size_kb, session, debug) - elif 'rss version="2.0"' in xml_str: + if 'rss version="2.0"' in xml_str: return _xml2str_to_dict(base_dir, domain, xml_str, moderated, mirrored, max_posts_per_source, max_feed_item_size_kb, max_categories_feedItem_size_kb, session, debug) - elif '= max_blogs_per_account: diff --git a/outbox.py b/outbox.py index ed4f0f1b0..1a8945ba8 100644 --- a/outbox.py +++ b/outbox.py @@ -143,7 +143,7 @@ def _person_receive_update_outbox(recent_posts_cache: {}, if 'attachment' not in actor_json: continue found = False - for attach_idx in range(len(actor_json['attachment'])): + for attach_idx, _ in enumerate(actor_json['attachment']): if actor_json['attachment'][attach_idx]['type'] != \ 'PropertyValue': continue diff --git a/person.py b/person.py index f0f2406d5..da54a191f 100644 --- a/person.py +++ b/person.py @@ -797,7 +797,7 @@ def person_upgrade_actor(base_dir: str, person_json: {}, update_actor = True else: # add location if it is missing - for index in range(len(person_json['hasOccupation'])): + for index, _ in enumerate(person_json['hasOccupation']): oc_item = person_json['hasOccupation'][index] if oc_item.get('hasOccupation'): oc_item = oc_item['hasOccupation'] diff --git a/roles.py b/roles.py index 770f0c1c3..f638d15db 100644 --- a/roles.py +++ b/roles.py @@ -163,7 +163,7 @@ def _set_actor_role(actor_json: {}, role_name: str) -> bool: if not category: return False - for index in range(len(actor_json['hasOccupation'])): + for index, _ in enumerate(actor_json['hasOccupation']): occupation_item = actor_json['hasOccupation'][index] if not isinstance(occupation_item, dict): continue diff --git a/speaker.py b/speaker.py index 5bb896f90..984f30a62 100644 --- a/speaker.py +++ b/speaker.py @@ -503,7 +503,7 @@ def _post_to_speaker_json(base_dir: str, http_prefix: str, follows = fp_foll.readlines() if len(follows) > 0: follow_requests_exist = True - for i in range(len(follows)): + for i, _ in enumerate(follows): follows[i] = follows[i].strip() follow_requests_list = follows post_dm = False diff --git a/tests.py b/tests.py index c5626da89..f549b8184 100644 --- a/tests.py +++ b/tests.py @@ -150,6 +150,8 @@ from linked_data_sig import generate_json_signature from linked_data_sig import verify_json_signature from newsdaemon import hashtag_rule_tree from newsdaemon import hashtag_rule_resolve +from newswire import get_link_from_rss_item +from newswire import xml_podcast_to_dict from newswire import get_newswire_tags from newswire import parse_feed_date from newswire import limit_word_lengths @@ -6354,7 +6356,7 @@ def _test_httpsig_base_new(with_digest: bool, base_dir: str, def _test_get_actor_from_in_reply_to() -> None: - print('testGetActorFromInReplyTo') + print('test_get_actor_from_in_reply_to') in_reply_to = \ 'https://fosstodon.org/users/bashrc/statuses/107400700612621140' reply_actor = get_actor_from_in_reply_to(in_reply_to) @@ -6365,6 +6367,117 @@ def _test_get_actor_from_in_reply_to() -> None: assert reply_actor is None +def _test_xml_podcast_dict() -> None: + print('test_xml_podcast_dict') + xml_str = \ + '\n' + \ + '\n' + \ + '5\n' + \ + '\n' + \ + '' + \ + 'Support the show\n' + \ + '\n' + \ + '' + \ + 'Nowheresville\n' + \ + 'yes' + \ + '\n' + \ + '' + \ + 'Rodger Rabbit\n' + \ + 'Rodger Rabbit' + \ + '\n' + \ + '' + \ + 'Jessica Rabbit\n' + \ + '' + \ + 'Betty Boop\n' + \ + '' + \ + 'Bob Hoskins\n' + \ + '1\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + '\n' + \ + '' + podcast_properties = xml_podcast_to_dict(xml_str) + assert podcast_properties + # pprint(podcast_properties) + assert podcast_properties.get('valueRecipients') + assert podcast_properties.get('persons') + assert podcast_properties.get('soundbites') + assert podcast_properties.get('locations') + assert podcast_properties.get('transcripts') + assert podcast_properties.get('episode') + 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 + assert len(podcast_properties['locations']) == 1 + + +def _test_get_link_from_rss_item() -> None: + print('test_get_link_from_rssitem') + rss_item = \ + '' + \ + 'https://anchor.fm/creativecommons/episodes/' + \ + 'Hessel-van-Oorschot-of-Tribe-of-Noise--Free-Music-Archive-e1crvce' + \ + '' + \ + 'Wed, 12 Jan 2022 14:28:46 GMT' + \ + '' + link, mime_type = get_link_from_rss_item(rss_item) + assert link + assert link.endswith('.mp3') + assert mime_type + assert mime_type == 'audio/mpeg' + + rss_item = \ + '' + \ + 'https://anchor.fm/creativecommons/episodes/' + \ + 'Hessel-van-Oorschot-of-Tribe-of-Noise--Free-Music-Archive-e1crvce' + \ + '' + \ + 'Wed, 12 Jan 2022 14:28:46 GMT' + link, mime_type = get_link_from_rss_item(rss_item) + assert link + assert link.startswith('https://anchor.fm') + assert not mime_type + + def run_all_tests(): base_dir = os.getcwd() print('Running tests...') @@ -6381,6 +6494,8 @@ def run_all_tests(): 'message_json', 'liked_post_json']) _test_checkbox_names() _test_functions() + _test_get_link_from_rss_item() + _test_xml_podcast_dict() _test_get_actor_from_in_reply_to() _test_valid_emoji_content() _test_add_cw_lists(base_dir) diff --git a/theme.py b/theme.py index 91e054ee0..25cb40619 100644 --- a/theme.py +++ b/theme.py @@ -107,7 +107,7 @@ def _get_theme_files() -> []: return ('epicyon.css', 'login.css', 'follow.css', 'suspended.css', 'calendar.css', 'blog.css', 'options.css', 'search.css', 'links.css', - 'welcome.css', 'graph.css') + 'welcome.css', 'graph.css', 'podcast.css') def is_news_theme_name(base_dir: str, theme_name: str) -> bool: diff --git a/utils.py b/utils.py index 5d0b7f565..3e1930464 100644 --- a/utils.py +++ b/utils.py @@ -559,7 +559,7 @@ def get_followers_list(base_dir: str, with open(filename, 'r') as foll_file: lines = foll_file.readlines() - for i in range(len(lines)): + for i, _ in enumerate(lines): lines[i] = lines[i].strip() return lines return [] @@ -2126,7 +2126,7 @@ def _search_virtual_box_posts(base_dir: str, nickname: str, domain: str, if '+' in search_str: search_words = search_str.split('+') - for index in range(len(search_words)): + for index, _ in enumerate(search_words): search_words[index] = search_words[index].strip() print('SEARCH: ' + str(search_words)) else: @@ -2178,7 +2178,7 @@ def search_box_posts(base_dir: str, nickname: str, domain: str, if '+' in search_str: search_words = search_str.split('+') - for index in range(len(search_words)): + for index, _ in enumerate(search_words): search_words[index] = search_words[index].strip() print('SEARCH: ' + str(search_words)) else: @@ -2811,7 +2811,7 @@ def set_occupation_name(actor_json: {}, name: str) -> bool: return False if not isinstance(actor_json['hasOccupation'], list): return False - for index in range(len(actor_json['hasOccupation'])): + for index, _ in enumerate(actor_json['hasOccupation']): occupation_item = actor_json['hasOccupation'][index] if not isinstance(occupation_item, dict): continue @@ -2831,7 +2831,7 @@ def set_occupation_skills_list(actor_json: {}, skills_list: []) -> bool: return False if not isinstance(actor_json['hasOccupation'], list): return False - for index in range(len(actor_json['hasOccupation'])): + for index, _ in enumerate(actor_json['hasOccupation']): occupation_item = actor_json['hasOccupation'][index] if not isinstance(occupation_item, dict): continue diff --git a/webapp_column_right.py b/webapp_column_right.py index 1940f307d..9548eaaa8 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -263,6 +263,19 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool, '' moderated_item = item[5] + link_url = url + + # is this a podcast episode? + if len(item) > 8: + # change the link url to a podcast episode screen + podcast_properties = item[8] + if podcast_properties: + if podcast_properties.get('image'): + episode_id = date_str.replace(' ', '__') + episode_id = episode_id.replace(':', 'aa') + link_url = \ + '/users/' + nickname + '/?podepisode=' + episode_id + html_str += separator_str if moderated_item and 'vote:' + nickname in item[2]: total_votes_str = '' @@ -275,7 +288,7 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool, title = remove_long_words(item[0], 16, []).replace('\n', '
') title = limit_repeated_words(title, 6) html_str += '

' + \ - '' + \ '' + \ favicon_link + title + '' + total_votes_str @@ -305,7 +318,7 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool, title = limit_repeated_words(title, 6) if moderator and moderated_item: html_str += '

' + \ - '' + \ favicon_link + title + '' + total_votes_str html_str += ' ' + date_shown @@ -318,7 +331,7 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool, html_str += '

\n' else: html_str += '

' + \ - '' + \ favicon_link + title + '' + total_votes_str html_str += ' ' diff --git a/webapp_podcast.py b/webapp_podcast.py new file mode 100644 index 000000000..def903fbb --- /dev/null +++ b/webapp_podcast.py @@ -0,0 +1,219 @@ +__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 +import html +import urllib.parse +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' + podcast_str += '
\n' + + podcast_str += '
\n' + audio_extension = None + if path_is_audio(link_url): + if '.mp3' in link_url: + audio_extension = 'mpeg' + else: + audio_extension = 'ogg' + else: + if podcast_properties.get('linkMimeType'): + if 'audio' in podcast_properties['linkMimeType']: + audio_extension = \ + podcast_properties['linkMimeType'].split('/')[1] + # show widgets for soundbites + if audio_extension: + podcast_str += _html_podcast_soundbites(link_url, audio_extension, + podcast_properties, + translate) + + # podcast player widget + podcast_str += \ + ' \n' + + podcast_title = \ + remove_html(html.unescape(urllib.parse.unquote_plus(newswire_item[0]))) + if podcast_title: + podcast_str += \ + '

\n' + if newswire_item[4]: + podcast_description = \ + html.unescape(urllib.parse.unquote_plus(newswire_item[4])) + podcast_description = remove_html(podcast_description) + if podcast_description: + remove_chars = ('Œ', 'â€', 'ğŸ', '�') + for remchar in remove_chars: + podcast_description = podcast_description.replace(remchar, '') + 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 diff --git a/webapp_search.py b/webapp_search.py index d8f3df883..32b3ec17d 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -941,9 +941,8 @@ def rss_hashtag_search(nickname: str, domain: str, port: int, domain_full = get_full_domain(domain, port) max_feed_length = 10 - hashtag_feed = \ - rss2tag_header(hashtag, http_prefix, domain_full) - for index in range(len(lines)): + hashtag_feed = rss2tag_header(hashtag, http_prefix, domain_full) + for index, _ in enumerate(lines): post_id = lines[index].strip('\n').strip('\r') if ' ' not in post_id: nickname = get_nickname_from_actor(post_id)