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

merge-requests/30/head
Bob Mottram 2022-05-03 18:53:18 +01:00
commit 83de3d846a
26 changed files with 253 additions and 49 deletions

View File

@ -15626,7 +15626,12 @@ class PubServer(BaseHTTPRequestHandler):
self.server.theme_name,
self.server.default_timeline,
self.server.text_mode_banner,
self.server.access_keys)
self.server.access_keys,
self.server.session,
self.server.session_onion,
self.server.session_i2p,
self.server.http_prefix,
self.server.debug)
if html_str:
msg = html_str.encode('utf-8')
msglen = len(msg)

View File

@ -158,6 +158,10 @@ a:focus {
width: 15%;
}
.performers {
display: flex;
}
@media screen and (min-width: 400px) {
textarea {
font-family: Arial, Helvetica, sans-serif;
@ -233,6 +237,12 @@ a:focus {
padding: 10px;
margin: 20px 30px;
}
.performers img {
width: 20%;
}
.chapters img {
width: 10%;
}
}
@media screen and (max-width: 1000px) {
@ -310,6 +320,12 @@ a:focus {
padding: 20px;
margin: 30px 40px;
}
.performers img {
width: 30%;
}
.chapters img {
width: 15%;
}
}
@media screen and (max-width: 480px) {
@ -387,4 +403,10 @@ a:focus {
padding: 20px;
margin: 30px 40px;
}
.performers img {
width: 30%;
}
.chapters img {
width: 15%;
}
}

View File

@ -515,6 +515,7 @@ def xml_podcast_to_dict(base_dir: str, xml_item: str, xml_str: str) -> {}:
"transcripts": [],
"valueRecipients": [],
"trailers": [],
"chapters": [],
"discussion": [],
"episode": '',
"socialInteract": [],

View File

@ -550,5 +550,6 @@
"Common emoji": "الرموز التعبيرية الشائعة",
"Copy and paste into your text": "نسخ ولصق في النص الخاص بك",
"shrug": "هز كتفيه",
"DM warning": "لا يتم تشفير الرسائل المباشرة من طرف إلى طرف. لا تشارك أي معلومات حساسة للغاية هنا."
"DM warning": "لا يتم تشفير الرسائل المباشرة من طرف إلى طرف. لا تشارك أي معلومات حساسة للغاية هنا.",
"Transcript": "نص"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji comú",
"Copy and paste into your text": "Copia i enganxa al teu text",
"shrug": "arronsar les espatlles",
"DM warning": "Els missatges directes no estan xifrats d'extrem a extrem. No compartiu cap informació molt sensible aquí."
"DM warning": "Els missatges directes no estan xifrats d'extrem a extrem. No compartiu cap informació molt sensible aquí.",
"Transcript": "Transcripció"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji cyffredin",
"Copy and paste into your text": "Copïwch a gludwch i'ch testun",
"shrug": "shrug",
"DM warning": "Nid yw negeseuon uniongyrchol wedi'u hamgryptio o'r dechrau i'r diwedd. Peidiwch â rhannu unrhyw wybodaeth hynod sensitif yma."
"DM warning": "Nid yw negeseuon uniongyrchol wedi'u hamgryptio o'r dechrau i'r diwedd. Peidiwch â rhannu unrhyw wybodaeth hynod sensitif yma.",
"Transcript": "Trawsgrifiad"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Gewöhnliches Emoji",
"Copy and paste into your text": "Kopieren und in Ihren Text einfügen",
"shrug": "zucken",
"DM warning": "Direktnachrichten sind nicht Ende-zu-Ende verschlüsselt. Geben Sie hier keine hochsensiblen Informationen weiter."
"DM warning": "Direktnachrichten sind nicht Ende-zu-Ende verschlüsselt. Geben Sie hier keine hochsensiblen Informationen weiter.",
"Transcript": "Abschrift"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Κοινά emoji",
"Copy and paste into your text": "Αντιγράψτε και επικολλήστε στο κείμενό σας",
"shrug": "σήκωμα των ώμων",
"DM warning": "Τα άμεσα μηνύματα δεν είναι κρυπτογραφημένα από άκρο σε άκρο. Μην μοιράζεστε καμία εξαιρετικά ευαίσθητη πληροφορία εδώ."
"DM warning": "Τα άμεσα μηνύματα δεν είναι κρυπτογραφημένα από άκρο σε άκρο. Μην μοιράζεστε καμία εξαιρετικά ευαίσθητη πληροφορία εδώ.",
"Transcript": "Αντίγραφο"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Common emoji",
"Copy and paste into your text": "Copy and paste into your text",
"shrug": "shrug",
"DM warning": "Direct messages are not end-to-end encrypted. Do not share any highly sensitive information here."
"DM warning": "Direct messages are not end-to-end encrypted. Do not share any highly sensitive information here.",
"Transcript": "Transcript"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoticonos comunes",
"Copy and paste into your text": "Copia y pega en tu texto",
"shrug": "encogimiento de hombros",
"DM warning": "Los mensajes directos no están cifrados de extremo a extremo. No comparta ninguna información altamente confidencial aquí."
"DM warning": "Los mensajes directos no están cifrados de extremo a extremo. No comparta ninguna información altamente confidencial aquí.",
"Transcript": "Transcripción"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Émoji commun",
"Copy and paste into your text": "Copiez et collez dans votre texte",
"shrug": "hausser les épaules",
"DM warning": "Les messages directs ne sont pas chiffrés de bout en bout. Ne partagez aucune information hautement sensible ici."
"DM warning": "Les messages directs ne sont pas chiffrés de bout en bout. Ne partagez aucune information hautement sensible ici.",
"Transcript": "Transcription"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji coitianta",
"Copy and paste into your text": "Cóipeáil agus greamaigh isteach i do théacs",
"shrug": "shrug",
"DM warning": "Níl teachtaireachtaí díreacha criptithe ó cheann go ceann. Ná roinn aon fhaisnéis an-íogair anseo."
"DM warning": "Níl teachtaireachtaí díreacha criptithe ó cheann go ceann. Ná roinn aon fhaisnéis an-íogair anseo.",
"Transcript": "Athscríbhinn"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "आम इमोजी",
"Copy and paste into your text": "अपने टेक्स्ट में कॉपी और पेस्ट करें",
"shrug": "कंधे उचकाने की क्रिया",
"DM warning": "डायरेक्ट मैसेज एंड-टू-एंड एन्क्रिप्टेड नहीं होते हैं। यहां कोई अति संवेदनशील जानकारी साझा न करें।"
"DM warning": "डायरेक्ट मैसेज एंड-टू-एंड एन्क्रिप्टेड नहीं होते हैं। यहां कोई अति संवेदनशील जानकारी साझा न करें।",
"Transcript": "प्रतिलिपि"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji comuni",
"Copy and paste into your text": "Copia e incolla nel tuo testo",
"shrug": "scrollare le spalle",
"DM warning": "I messaggi diretti non sono crittografati end-to-end. Non condividere qui alcuna informazione altamente sensibile."
"DM warning": "I messaggi diretti non sono crittografati end-to-end. Non condividere qui alcuna informazione altamente sensibile.",
"Transcript": "Trascrizione"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "一般的な絵文字",
"Copy and paste into your text": "コピーしてテキストに貼り付けます",
"shrug": "肩をすくめる",
"DM warning": "ダイレクトメッセージはエンドツーエンドで暗号化されません。 ここでは機密性の高い情報を共有しないでください。"
"DM warning": "ダイレクトメッセージはエンドツーエンドで暗号化されません。 ここでは機密性の高い情報を共有しないでください。",
"Transcript": "トランスクリプト"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "일반적인 이모티콘",
"Copy and paste into your text": "텍스트에 복사하여 붙여넣기",
"shrug": "어깨를 으쓱하다",
"DM warning": "다이렉트 메시지는 종단 간 암호화되지 않습니다. 여기에 매우 민감한 정보를 공유하지 마십시오."
"DM warning": "다이렉트 메시지는 종단 간 암호화되지 않습니다. 여기에 매우 민감한 정보를 공유하지 마십시오.",
"Transcript": "성적 증명서"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emojiyên hevpar",
"Copy and paste into your text": "Di nivîsa xwe de kopî bikin û bixin",
"shrug": "şuştin",
"DM warning": "Peyamên rasterast bi dawî-bi-dawî ne şîfrekirî ne. Li vir agahdariya pir hesas parve nekin."
"DM warning": "Peyamên rasterast bi dawî-bi-dawî ne şîfrekirî ne. Li vir agahdariya pir hesas parve nekin.",
"Transcript": "Transcript"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Gemeenschappelijke emoji",
"Copy and paste into your text": "Kopieer en plak in je tekst",
"shrug": "schouderophalend",
"DM warning": "Directe berichten zijn niet end-to-end versleuteld. Deel hier geen zeer gevoelige informatie."
"DM warning": "Directe berichten zijn niet end-to-end versleuteld. Deel hier geen zeer gevoelige informatie.",
"Transcript": "Vertaling"
}

View File

@ -546,5 +546,6 @@
"Common emoji": "Common emoji",
"Copy and paste into your text": "Copy and paste into your text",
"shrug": "shrug",
"DM warning": "Direct messages are not end-to-end encrypted. Do not share any highly sensitive information here."
"DM warning": "Direct messages are not end-to-end encrypted. Do not share any highly sensitive information here.",
"Transcript": "Transcript"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Popularne emotikony",
"Copy and paste into your text": "Skopiuj i wklej do swojego tekstu",
"shrug": "wzruszać ramionami",
"DM warning": "Wiadomości na czacie nie są szyfrowane metodą end-to-end. Nie udostępniaj tutaj żadnych wysoce wrażliwych informacji."
"DM warning": "Wiadomości na czacie nie są szyfrowane metodą end-to-end. Nie udostępniaj tutaj żadnych wysoce wrażliwych informacji.",
"Transcript": "Transkrypcja"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji comum",
"Copy and paste into your text": "Copie e cole no seu texto",
"shrug": "dar de ombros",
"DM warning": "As mensagens diretas não são criptografadas de ponta a ponta. Não compartilhe nenhuma informação altamente sensível aqui."
"DM warning": "As mensagens diretas não são criptografadas de ponta a ponta. Não compartilhe nenhuma informação altamente sensível aqui.",
"Transcript": "Transcrição"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Общие смайлики",
"Copy and paste into your text": "Скопируйте и вставьте в свой текст",
"shrug": "пожимание плечами",
"DM warning": "Прямые сообщения не подвергаются сквозному шифрованию. Не делитесь здесь особо конфиденциальной информацией."
"DM warning": "Прямые сообщения не подвергаются сквозному шифрованию. Не делитесь здесь особо конфиденциальной информацией.",
"Transcript": "Стенограмма"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Emoji ya kawaida",
"Copy and paste into your text": "Nakili na ubandike kwenye maandishi yako",
"shrug": "piga mabega",
"DM warning": "Ujumbe wa moja kwa moja haujasimbwa kutoka mwisho hadi mwisho. Usishiriki maelezo yoyote nyeti sana hapa."
"DM warning": "Ujumbe wa moja kwa moja haujasimbwa kutoka mwisho hadi mwisho. Usishiriki maelezo yoyote nyeti sana hapa.",
"Transcript": "Nakala"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "Звичайні емодзі",
"Copy and paste into your text": "Скопіюйте та вставте у свій текст",
"shrug": "знизати плечима",
"DM warning": "Прямі повідомлення не наскрізне шифруються. Не публікуйте тут дуже конфіденційну інформацію."
"DM warning": "Прямі повідомлення не наскрізне шифруються. Не публікуйте тут дуже конфіденційну інформацію.",
"Transcript": "Стенограма"
}

View File

@ -550,5 +550,6 @@
"Common emoji": "常见表情符号",
"Copy and paste into your text": "复制并粘贴到您的文本中",
"shrug": "耸耸肩",
"DM warning": "直接消息不是端到端加密的。 不要在这里分享任何高度敏感的信息。"
"DM warning": "直接消息不是端到端加密的。 不要在这里分享任何高度敏感的信息。",
"Transcript": "成绩单"
}

View File

@ -9,6 +9,7 @@ __module_group__ = "Web Interface Columns"
import os
import html
import datetime
import urllib.parse
from shutil import copyfile
from utils import get_config_param
@ -19,6 +20,121 @@ 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
from session import get_json
def _html_podcast_chapters(link_url: str,
session, session_onion, session_i2p,
http_prefix: str, domain: str,
podcast_properties: {}, translate: {},
debug: bool) -> str:
"""Returns html for chapters of a podcast
"""
if not podcast_properties:
return ''
key = 'chapters'
if not podcast_properties.get(key):
return ''
if not isinstance(podcast_properties[key], dict):
return ''
if podcast_properties[key].get('url'):
chapters_url = podcast_properties[key]['url']
elif podcast_properties[key].get('uri'):
chapters_url = podcast_properties[key]['uri']
else:
return ''
html_str = ''
if podcast_properties[key].get('type'):
url_type = podcast_properties[key]['type']
curr_session = session
if chapters_url.endswith('.onion'):
curr_session = session_onion
elif chapters_url.endswith('.i2p'):
curr_session = session_i2p
as_header = {
'Accept': url_type
}
if 'json' in url_type:
chapters_json = \
get_json(None, curr_session, chapters_url,
as_header, None, debug, __version__,
http_prefix, domain)
if not chapters_json:
return ''
if not chapters_json.get('chapters'):
return ''
if not isinstance(chapters_json['chapters'], list):
return ''
chapters_html = ''
for chapter in chapters_json['chapters']:
if not isinstance(chapter, dict):
continue
if not chapter.get('title'):
continue
if not chapter.get('startTime'):
continue
chapter_title = chapter['title']
chapter_url = ''
if chapter.get('url'):
chapter_url = chapter['url']
chapter_title = \
'<a href="' + chapter_url + '">' + \
chapter['title'] + '<\a>'
start_sec = chapter['startTime']
skip_url = link_url + '#t=' + str(start_sec)
start_time_str = \
'<a href="' + skip_url + '">' + \
str(datetime.timedelta(seconds=start_sec)) + \
'</a>'
if chapter.get('img'):
chapters_html += \
' <li>\n' + \
' ' + start_time_str + '\n' + \
' <img loading="lazy" ' + \
'decoding="async" ' + \
'src="' + chapter['img'] + \
'" alt="" />\n' + \
' ' + chapter_title + '\n' + \
' </li>\n'
if chapters_html:
html_str = \
'<div class="chapters">\n' + \
' <ul>\n' + chapters_html + ' </ul>\n</div>\n'
return html_str
def _html_podcast_transcripts(podcast_properties: {}, translate: {}) -> str:
"""Returns html for transcripts of a podcast
"""
if not podcast_properties:
return ''
key = 'transcripts'
if not podcast_properties.get(key):
return ''
if not isinstance(podcast_properties[key], list):
return ''
ctr = 1
html_str = ''
for transcript in podcast_properties[key]:
transcript_url = None
if podcast_properties[key].get('url'):
transcript_url = podcast_properties[key]['url']
elif podcast_properties[key].get('uri'):
transcript_url = podcast_properties[key]['uri']
if not transcript_url:
continue
if ctr > 1:
html_str += '<br>'
html_str += '<a href="' + transcript_url + '">'
html_str += translate['Transcript']
if ctr > 1:
html_str += ' ' + str(ctr)
html_str += '</a>\n'
ctr += 1
return html_str
def _html_podcast_social_interactions(podcast_properties: {},
@ -33,6 +149,8 @@ def _html_podcast_social_interactions(podcast_properties: {},
key = 'socialInteract'
if not podcast_properties.get(key):
return ''
if not isinstance(podcast_properties[key], dict):
return ''
if podcast_properties[key].get('uri'):
episode_post_url = podcast_properties[key]['uri']
elif podcast_properties[key].get('url'):
@ -60,9 +178,10 @@ def _html_podcast_social_interactions(podcast_properties: {},
'?replyto=' + episode_post_url + actor_str + '" target="_blank" ' + \
'rel="nofollow noopener noreferrer">💬 ' + \
translate['Leave a comment'] + '</a>\n' + \
' <span itemprop="comment">\n' + \
' <a href="' + episode_post_url + '" target="_blank" ' + \
'rel="nofollow noopener noreferrer">' + \
translate['View comments'] + '</a>\n' + \
translate['View comments'] + '</a>\n </span>\n' + \
'</center>\n'
return podcast_str
@ -72,21 +191,27 @@ def _html_podcast_performers(podcast_properties: {}) -> str:
"""
if not podcast_properties:
return ''
if not podcast_properties.get('persons'):
key = 'persons'
if not podcast_properties.get(key):
return ''
if not isinstance(podcast_properties[key], list):
return ''
# list of performers
podcast_str = '<div class="performers">\n'
podcast_str += ' <center>\n'
podcast_str += '<ul>\n'
for performer in podcast_properties['persons']:
for performer in podcast_properties[key]:
if not performer.get('text'):
continue
performer_name = performer['text']
performer_name = \
'<span itemprop="name">' + performer['text'] + '</span>'
performer_title = performer_name
if performer.get('role'):
performer_title += ' (' + performer['role'] + ')'
performer_title += \
' (<span itemprop="hasOccupation">' + \
performer['role'] + '</span>)'
if performer.get('group'):
performer_title += ', <i>' + performer['group'] + '</i>'
performer_title = remove_html(performer_title)
@ -101,14 +226,17 @@ def _html_podcast_performers(podcast_properties: {}) -> str:
podcast_str += ' <li>\n'
podcast_str += ' <figure>\n'
podcast_str += ' <a href="' + performer_url + '">\n'
podcast_str += ' <span itemprop="creator" ' + \
'itemscope itemtype="https://schema.org/Person">\n'
podcast_str += \
' <a href="' + performer_url + '" itemprop="url">\n'
podcast_str += \
' <img loading="lazy" decoding="async" ' + \
'src="' + performer_img + '" alt="" />\n'
'src="' + performer_img + '" alt="" itemprop="image" />\n'
podcast_str += \
' <figcaption>' + performer_title + '</figcaption>\n'
podcast_str += ' </a>\n'
podcast_str += ' </figure>\n'
podcast_str += ' </span></figure>\n'
podcast_str += ' </li>\n'
podcast_str += '</ul>\n'
@ -149,12 +277,13 @@ def _html_podcast_soundbites(link_url: str, extension: str,
if ctr > 0:
soundbite_title += ' ' + str(ctr)
podcast_str += \
' <span itemprop="trailer">\n' + \
' <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'
'</audio>\n </span>\n'
podcast_str += ' </li>\n'
ctr += 1
@ -167,7 +296,9 @@ 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:
text_mode_banner: str, access_keys: {},
session, session_onion, session_i2p,
http_prefix: str, debug: bool) -> str:
"""Returns html for a podcast episode, giebn an item from the newswire
"""
css_filename = base_dir + '/epicyon-podcast.css'
@ -197,18 +328,22 @@ def html_podcast_episode(css_cache: {}, translate: {},
podcast_str += html_keyboard_navigation(text_mode_banner, {}, {})
podcast_str += '<br><br>\n'
podcast_str += '<div class="options">\n'
podcast_str += \
'<div class="options" itemscope ' + \
'itemtype="http://schema.org/PodcastEpisode">\n'
podcast_str += ' <div class="optionsAvatar">\n'
podcast_str += ' <center>\n'
podcast_str += ' <a href="' + link_url + '">\n'
podcast_str += ' <a href="' + link_url + '" itemprop="url">\n'
podcast_str += ' <span itemprop="image">\n'
if image_src == 'srcset':
podcast_str += ' <img loading="lazy" decoding="async" ' + \
'srcset="' + image_url + \
'" alt="" ' + get_broken_link_substitute() + '/></a>\n'
'" alt="" ' + get_broken_link_substitute() + '/>\n'
else:
podcast_str += ' <img loading="lazy" decoding="async" ' + \
'src="' + image_url + \
'" alt="" ' + get_broken_link_substitute() + '/></a>\n'
'" alt="" ' + get_broken_link_substitute() + '/>\n'
podcast_str += ' </span></a>\n'
podcast_str += ' </center>\n'
podcast_str += ' </div>\n'
@ -236,11 +371,12 @@ def html_podcast_episode(css_cache: {}, translate: {},
# podcast player widget
podcast_str += \
' <span itemprop="audio">\n' + \
' <audio controls>\n' + \
' <source src="' + link_url + '" type="audio/' + \
audio_extension.replace('.', '') + '">' + \
translate['Your browser does not support the audio element.'] + \
'\n </audio>\n'
'\n </audio>\n </span>\n'
elif podcast_properties.get('linkMimeType'):
if '/youtube' in podcast_properties['linkMimeType']:
url = link_url.replace('/watch?v=', '/embed/')
@ -249,55 +385,73 @@ def html_podcast_episode(css_cache: {}, translate: {},
if '?utm_' in url:
url = url.split('?utm_')[0]
podcast_str += \
' <span itemprop="video">\n' + \
" <iframe loading=\"lazy\" decoding=\"async\" src=\"" + \
url + "\" width=\"400\" height=\"300\" " + \
"frameborder=\"0\" allow=\"fullscreen\" " + \
"allowfullscreen>\n </iframe>\n"
"allowfullscreen>\n </iframe>\n </span>\n"
elif 'video' in podcast_properties['linkMimeType']:
video_mime_type = podcast_properties['linkMimeType']
video_msg = 'Your browser does not support the video element.'
podcast_str += \
' <span itemprop="video">\n' + \
' <figure id="videoContainer" ' + \
'data-fullscreen="false">\n' + \
' <video id="video" controls preload="metadata">\n' + \
'<source src="' + link_url + '" ' + \
'type="' + video_mime_type + '">' + \
translate[video_msg] + '</video>\n </figure>\n'
translate[video_msg] + \
'</video>\n </figure>\n </span>\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'
'<p><label class="podcast-title">' + \
'<span itemprop="headline">' + \
podcast_title + \
'</span></label></p>\n'
transcripts = _html_podcast_transcripts(podcast_properties, translate)
if transcripts:
podcast_str += '<p>' + transcripts + '</p>\n'
if newswire_item[4]:
podcast_description = \
html.unescape(urllib.parse.unquote_plus(newswire_item[4]))
podcast_description = safe_web_text(podcast_description)
if podcast_description:
podcast_str += '<p>' + podcast_description + '</p>\n'
podcast_str += \
'<p><span itemprop="description">' + \
podcast_description + '</span></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 + \
'<p><span itemprop="funding"><a href="' + donate_url + \
'"><button class="donateButton">' + translate['Donate'] + \
'</button></a></p>\n'
'</button></a></span></p>\n'
if podcast_properties['categories']:
podcast_str += '<p>'
tags_str = ''
for tag in podcast_properties['categories']:
tag_link = '/users/' + nickname + '/tags/' + tag.replace('#', '')
tags_str += '<a href="' + tag_link + '">' + tag + '</a> '
podcast_str += tags_str.strip() + '</p>\n'
tag = tag.replace('#', '')
tag_link = '/users/' + nickname + '/tags/' + tag
tags_str += \
'#<a href="' + tag_link + '">' + \
'<span itemprop="keywords">' + tag + '</span>' + \
'</a> '
podcast_str += '<p>' + tags_str.strip() + '</p>\n'
podcast_str += _html_podcast_performers(podcast_properties)
podcast_str += \
_html_podcast_social_interactions(podcast_properties, translate,
nickname)
podcast_str += \
_html_podcast_chapters(link_url,
session, session_onion, session_i2p,
http_prefix, domain,
podcast_properties, translate, debug)
podcast_str += ' </center>\n'
podcast_str += '</div>\n'