Merge branch 'main' of gitlab.com:bashrc2/epicyon

merge-requests/30/head
Bob Mottram 2022-01-12 21:23:00 +00:00
commit 1a3586b58d
16 changed files with 995 additions and 78 deletions

View File

@ -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()

View File

@ -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.

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,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:

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

@ -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,
'<br><a href="' + post_url + '">' + \
translate['Read more...'] + '</a>'
# 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:

View File

@ -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 '<podcast:' not in xml_str:
if '<itunes:' not in xml_str:
return {}
podcast_properties = {
"locations": [],
"persons": [],
"soundbites": [],
"transcripts": [],
"valueRecipients": [],
"trailers": []
}
pod_lines = xml_str.split('<podcast:')
ctr = 0
for pod_line in pod_lines:
if ctr == 0 or '>' 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 = ['<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_episode_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_episode_image = episode_image
break
if podcast_episode_image:
podcast_properties['image'] = podcast_episode_image
if '<itunes:explicit>Y' in xml_str or \
'<itunes:explicit>T' in xml_str or \
'<itunes:explicit>1' in xml_str:
podcast_properties['explicit'] = True
else:
podcast_properties['explicit'] = False
else:
if '<podcast:' not in xml_str:
return {}
return podcast_properties
def get_link_from_rss_item(rss_item: str) -> (str, str):
"""Extracts rss link from rss item string
"""
mime_type = None
if '<enclosure ' in rss_item:
# get link from audio or video enclosure
enclosure = rss_item.split('<enclosure ')[1]
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('<link>')[1]
link = link.split('</link>')[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 '</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]
@ -434,13 +570,15 @@ 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:
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('<pubDate>')[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('<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:
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('<dc:date>')[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('<media:description>')[1]
description = description.split('</media:description>')[0]
description = remove_html(description)
link = atom_item.split('<link>')[1]
link = link.split('</link>')[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('<updated>')[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 '<?xml version="1.0"' in xml_str:
if '<?xml version="1.0"' in xml_str:
return _xml1str_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 'xmlns="http://www.w3.org/2005/Atom"' in xml_str:
if 'xmlns="http://www.w3.org/2005/Atom"' in xml_str:
return _atom_feed_to_dict(base_dir, domain,
xml_str, moderated, mirrored,
max_posts_per_source, max_feed_item_size_kb,
session, debug)
elif 'https://jsonfeed.org/version/1' in xml_str:
if 'https://jsonfeed.org/version/1' in xml_str:
return _json_feed_v1to_dict(base_dir, domain,
xml_str, moderated, mirrored,
max_posts_per_source,
@ -1082,7 +1235,8 @@ def _add_account_blogs_to_newswire(base_dir: str, nickname: str, domain: str,
votes, full_post_filename,
description, moderated, False,
tags_from_post,
max_tags, session, debug)
max_tags, session, debug,
None)
ctr += 1
if ctr >= max_blogs_per_account:

View File

@ -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

View File

@ -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']

View File

@ -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

View File

@ -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

117
tests.py
View File

@ -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 = \
'<?xml version="1.0" encoding="UTF-8" ?>\n' + \
'<rss version="2.0" xmlns:podcast="' + \
'https://podcastindex.org/namespace/1.0">\n' + \
'<podcast:episode>5</podcast:episode>\n' + \
'<podcast:chapters ' + \
'url="https://whoframed.rodger/ep1_chapters.json" ' + \
'type="application/json"/>\n' + \
'<podcast:funding ' + \
'url="https://whoframed.rodger/donate">' + \
'Support the show</podcast:funding>\n' + \
'<podcast:images ' + \
'srcset="https://whoframed.rodger/images/ep1/' + \
'pci_avatar-massive.jpg 1500w, ' + \
'https://whoframed.rodger/images/ep1/pci_avatar-middle.jpg 600w, ' + \
'https://whoframed.rodger/images/ep1/pci_avatar-small.jpg 300w, ' + \
'https://whoframed.rodger/images/ep1/' + \
'pci_avatar-microfiche.jpg 50w" />\n' + \
'<podcast:location geo="geo:57.4272,34.63763" osm="R472152">' + \
'Nowheresville</podcast:location>\n' + \
'<podcast:locked owner="podcastowner@whoframed.rodger">yes' + \
'</podcast:locked>\n' + \
'<podcast:person group="visuals" role="cover art designer" ' + \
'href="https://whoframed.rodger/artist/rodgetrabbit">' + \
'Rodger Rabbit</podcast:person>\n' + \
'<podcast:person href="https://whoframed.rodger" ' + \
'img="http://whoframed.rodger/images/rr.jpg">Rodger Rabbit' + \
'</podcast:person>\n' + \
'<podcast:person href="https://whoframed.rodger" ' + \
'img="http://whoframed.rodger/images/jr.jpg">' + \
'Jessica Rabbit</podcast:person>\n' + \
'<podcast:person role="guest" ' + \
'href="https://whoframed.rodger/blog/bettyboop/" ' + \
'img="http://whoframed.rodger/images/bb.jpg">' + \
'Betty Boop</podcast:person>\n' + \
'<podcast:person role="guest" ' + \
'href="https://goodto.talk/bobhoskins/" ' + \
'img="https://goodto.talk/images/bhosk.jpg">' + \
'Bob Hoskins</podcast:person>\n' + \
'<podcast:season name="Podcasting 2.0">1</podcast:season>\n' + \
'<podcast:soundbite startTime="15.27" duration="8.0" />\n' + \
'<podcast:soundbite startTime="21.34" duration="32.0" />\n' + \
'<podcast:transcript ' + \
'url="https://whoframed.rodger/ep1/transcript.txt" ' + \
'type="text/plain" />\n' + \
'<podcast:transcript ' + \
'url="https://whoframed.rodger/ep2/transcript.txt" ' + \
'type="text/plain" />\n' + \
'<podcast:transcript ' + \
'url="https://whoframed.rodger/ep3/transcript.txt" ' + \
'type="text/plain" />\n' + \
'<podcast:value type="donate" method="keysend" ' + \
'suggested="2.95">\n' + \
' <podcast:valueRecipient name="hosting company" ' + \
'type="node" address="someaddress1" split="1" />\n' + \
' <podcast:valueRecipient name="podcaster" type="node" ' + \
'address="someaddress2" split="99" />\n' + \
'</podcast:value>\n' + \
'</rss>'
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 = \
'<link>' + \
'https://anchor.fm/creativecommons/episodes/' + \
'Hessel-van-Oorschot-of-Tribe-of-Noise--Free-Music-Archive-e1crvce' + \
'</link>' + \
'<pubDate>Wed, 12 Jan 2022 14:28:46 GMT</pubDate>' + \
'<enclosure url="https://anchor.fm/s/4d70d828/podcast/' + \
'play/46054222/https%3A%2F%2Fd3ctxlq1ktw2nl.cloudfront.net' + \
'%2Fstaging%2F2022-0-12%2F7352f28c-a928-ea7a-65ae-' + \
'ccb5edffbac1.mp3" length="67247880" type="audio/mpeg"/>'
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 = \
'<link>' + \
'https://anchor.fm/creativecommons/episodes/' + \
'Hessel-van-Oorschot-of-Tribe-of-Noise--Free-Music-Archive-e1crvce' + \
'</link>' + \
'<pubDate>Wed, 12 Jan 2022 14:28:46 GMT</pubDate>'
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)

View File

@ -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:

View File

@ -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

View File

@ -263,6 +263,19 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool,
'<img loading="lazy" src="' + favicon_url + '" ' + \
'alt="" ' + _get_broken_fav_substitute() + '/>'
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', '<br>')
title = limit_repeated_words(title, 6)
html_str += '<p class="newswireItemVotedOn">' + \
'<a href="' + url + '" target="_blank" ' + \
'<a href="' + link_url + '" target="_blank" ' + \
'rel="nofollow noopener noreferrer">' + \
'<span class="newswireItemVotedOn">' + \
favicon_link + title + '</span></a>' + 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 += '<p class="newswireItemModerated">' + \
'<a href="' + url + '" target="_blank" ' + \
'<a href="' + link_url + '" target="_blank" ' + \
'rel="nofollow noopener noreferrer">' + \
favicon_link + title + '</a>' + total_votes_str
html_str += ' ' + date_shown
@ -318,7 +331,7 @@ def _html_newswire(base_dir: str, newswire: {}, nickname: str, moderator: bool,
html_str += '</p>\n'
else:
html_str += '<p class="newswireItem">' + \
'<a href="' + url + '" target="_blank" ' + \
'<a href="' + link_url + '" target="_blank" ' + \
'rel="nofollow noopener noreferrer">' + \
favicon_link + title + '</a>' + total_votes_str
html_str += ' <span class="newswireDate">'

219
webapp_podcast.py 100644
View File

@ -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 = '<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 += ' </center>\n'
podcast_str += ' </div>\n'
podcast_str += ' <center>\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 += \
' <audio controls>\n' + \
' <source src="' + link_url + '" type="audio/' + \
audio_extension.replace('.', '') + '">' + \
translate['Your browser does not support the audio element.'] + \
'\n </audio>\n'
podcast_title = \
remove_html(html.unescape(urllib.parse.unquote_plus(newswire_item[0])))
if podcast_title:
podcast_str += \
'<p><label class="podcast-title">' + podcast_title + \
'</label></p>\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 = ('Œ', 'â€', 'ğŸ', '<EFBFBD>')
for remchar in remove_chars:
podcast_description = podcast_description.replace(remchar, '')
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

View File

@ -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)