diff --git a/python/podcast/podcast.py b/python/podcast/podcast.py index ae573fdfc08d660f9084dcbf9c2de491c78a54e5..f208d0a2342563c8c46f753140c8c9097c933c99 100644 --- a/python/podcast/podcast.py +++ b/python/podcast/podcast.py @@ -9,7 +9,7 @@ from feedparser import FeedParserDict from peewee import CharField, TextField, BooleanField, IntegerField, ModelSelect, DoesNotExist, FloatField, \ IntegrityError from podcast.persistent_log import persist_log, LogType -from podcast.util import movePost +from podcast.util import movePost, download_image_into_storage sys.path.append("/usr/share/podqast/python") @@ -28,7 +28,6 @@ from podcast import util, feedutils from email.utils import mktime_tz, parsedate_tz from calendar import timegm -import hashlib import os import logging import pyotherside @@ -132,9 +131,7 @@ class Podcast(BaseModel): self.logo_url = feed.feed.image.href logger.debug("podcast new logo_url: %s", self.logo_url) old_path = self.logo_path - if self.logo_path: - self.logo_path = None - self.__download_icon() + self.logo_path = download_image_into_storage(self.logo_url, Constants().iconpath) or None if old_path: util.delete_file(old_path) except: @@ -291,35 +288,6 @@ class Podcast(BaseModel): else: return None - def __download_icon(self): - """ - Download icon - """ - - iconpath = Constants().iconpath - rdot = self.logo_url.rfind(".") - extexcl = self.logo_url.rfind("?") - if extexcl > 0: - ext = self.logo_url[rdot + 1: extexcl] - else: - ext = self.logo_url[rdot + 1:] - if ext == "jpg" or ext == "jpeg" or ext == "png" or ext == "gif": - name = ( - hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext - ) - - logo_path = os.path.join(iconpath, name) - try: - util.dl_from_url(self.logo_url, logo_path) - except: - logo_path = "" - logger.info( - "could not load icon: %s", str(sys.exc_info()[0]) - ) - self.logo_path = logo_path - else: - self.logo_path = "" - def refresh(self, moveto, limit=0, full_refresh=False) -> Iterator[Tuple[int, 'Podpost']]: """ Refresh podcast and return list of new entries diff --git a/python/podcast/podpost.py b/python/podcast/podpost.py index cc47b9fd33ec94b225f32c49fcab6d2de5d8e658..a684e6174be112f1101ae8634d841234c5e58686 100644 --- a/python/podcast/podpost.py +++ b/python/podcast/podpost.py @@ -324,28 +324,15 @@ class Podpost(BaseModel): """ Download icon """ - + if not self.logo_url: + return if self.logo_path: if os.path.exists(self.logo_path): return - - iconpath = Constants().iconpath - rdot = self.logo_url.rfind(".") - ext = self.logo_url[rdot + 1:] - if ext == "jpg" or ext == "jpeg" or ext == "png" or ext == "gif": - name = ( - hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext - ) - - logo_path = os.path.join(iconpath, name) - try: - util.dl_from_url(self.logo_url, logo_path) - except: - logo_path = "" - - self.logo_path = logo_path - else: - self.logo_path = "" + try: + self.logo_path = util.download_image_into_storage(self.logo_url, Constants().iconpath) or None + except: + self.logo_path = None def play(self) -> Dict[str,object]: """ diff --git a/python/podcast/util.py b/python/podcast/util.py index da747262a1d9404834817e57991f5c41ca5b0390..c850a594d3fffde3b6fbd50fcc579edace816f26 100644 --- a/python/podcast/util.py +++ b/python/podcast/util.py @@ -1,23 +1,28 @@ """ some utilities """ +import base64 +import datetime +import hashlib import logging import os -import urllib.request -import urllib.error +import shutil +import subprocess +import tempfile import time -import datetime +import urllib.error +import urllib.request from collections import deque from enum import unique, IntEnum from itertools import count -from typing import List, Iterator, Tuple +from typing import List, Iterator, Tuple, Optional +from xml.dom import minidom from xml.etree import ElementTree as ET from xml.etree.ElementTree import Element, SubElement, Comment, tostring -from xml.dom import minidom + import pyotherside + import mutagen -import hashlib -import base64 from mutagen.flac import Picture, error as FLACError logger = logging.getLogger(__name__) @@ -114,20 +119,53 @@ def s_to_year(secs): return time.strftime("%Y-%m-%d", time.gmtime(secs)) -def dl_from_url(url, path): +def download_image_into_storage(url: str, iconpath: str) -> Optional[str]: + name = hashlib.sha256(url.encode()).hexdigest() + save_path = os.path.join(iconpath, name) + try: + download_image(url, save_path) + return save_path + except Exception: + logger.exception("could not load icon") + return None + + +def download_image(url, path, max_file_size_in_mb=20): """ - Download a file from url and save to path + downloads the image from url to path. checks if it is actually an image + :param url: url of the image + :param path: path to save to + :param max_file_size_in_mb: raises an exception if the file is larger than that + :return: nothing """ + req = urllib.request.Request(url, data=None, headers={"User-Agent": __get_http_agent(url)}) + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with urllib.request.urlopen(req) as response: + shutil.copyfileobj(response, tmp_file) + tmp_file.flush() + + filesize = get_file_length(tmp_file.name) + if filesize > 1024 * 1024 * max_file_size_in_mb: # panic if file is larger than 20 mb + raise ValueError("the downloaded file was had size %d bytes which is larger than %d bytes" % ( + filesize, max_file_size_in_mb * 1024 * 1024)) + __assert_is_image_type(tmp_file.name, url) + shutil.move(tmp_file.name, path) + + +def __assert_is_image_type(path: str, url: str): + return True # file util is not allowed with sailjail unfortunatley... + result = subprocess.check_output(['file', '--mime-type', path], text=True) + mime_type = result.split(maxsplit=1)[1] + if not mime_type.startswith("image/"): + raise ValueError("expected an image from '%s' on path %s but detected mimetype '%s'" % (url, path, mime_type)) + +def __get_http_agent(url): if url.find("tumblr.com") >= 0: agent = user_agent2 else: agent = user_agent - - rsp = urllib.request.Request(url, data=None, headers={"User-Agent": agent}) - response = urllib.request.urlopen(rsp) - with open(path, "wb") as fhandle: - fhandle.write(response.read()) + return agent def dl_from_url_progress(url, path): @@ -139,10 +177,7 @@ def dl_from_url_progress(url, path): count = 0 p1 = 0 - if url.find("tumblr.com") >= 0: - agent = user_agent2 - else: - agent = user_agent + agent = __get_http_agent(url) req = urllib.request.Request(url, data=None, headers={"User-Agent": agent}) try: @@ -415,6 +450,7 @@ def down_audio_icon(afile, downpath): return filepath + @unique class MovePostToPage(IntEnum): INBOX = 0 @@ -423,7 +459,7 @@ class MovePostToPage(IntEnum): HISTORY = 3 def get_humanized_name(self): - return ["Inbox","Queue","Queue","History"][self] + return ["Inbox", "Queue", "Queue", "History"][self] def movePost(move: int, postid): diff --git a/qml/components/EpisodeImage.qml b/qml/components/EpisodeImage.qml index f24ce90e279dfac44f6f4bc046e580cb803412c4..591de93b6f8b9aed3ebc5343bacb9914c68d7b8d 100644 --- a/qml/components/EpisodeImage.qml +++ b/qml/components/EpisodeImage.qml @@ -52,7 +52,8 @@ Thumbnail { height: Theme.iconSizeLarge sourceSize.width: Theme.iconSizeLarge sourceSize.height: Theme.iconSizeLarge - source: model.podcast_logo === "" ? "../../images/podcast.png" : model.podcast_logo + source: (model.podcast_logo === "" || model === undefined + || model.podcast_logo === undefined) ? "../../images/podcast.png" : model.podcast_logo Rectangle { id: dlstatus visible: showDownladingState diff --git a/qml/components/hints/HintEpisodeImage.qml b/qml/components/hints/HintEpisodeImage.qml index 496c7829a461a64224995382c1180b6908bd5eb1..9dfd64317f806b75ad4a796f5e4344d7c2b0d7a2 100644 --- a/qml/components/hints/HintEpisodeImage.qml +++ b/qml/components/hints/HintEpisodeImage.qml @@ -11,7 +11,6 @@ EpisodeImage { showIsListened: relativeIndex === 2 percentage: relativeIndex === 0 ? 100 : 0 anchors.fill: centeredComponent - property string logo_url: "../../images/podcast.png" - width: Theme.iconSizeExtraLarge - height: Theme.iconSizeExtraLarge + width: Theme.itemSizeHuge + height: Theme.itemSizeHuge } diff --git a/qml/components/hints/PlayerHint.qml b/qml/components/hints/PlayerHint.qml index d500d4f2e18681f352f8556b3b09363c23aa612b..78e85398f74368f6e0e6a0586e4fe7e200063f11 100644 --- a/qml/components/hints/PlayerHint.qml +++ b/qml/components/hints/PlayerHint.qml @@ -8,12 +8,12 @@ SlideShowHint { "text": qsTr("Select if the next item of the playlist should be played automatically"), "direction": TouchInteraction.Down, "interactionMode": TouchInteraction.EdgeSwipe, - "loops": 3 + "loops": 0 }, { "text": qsTr("Click the image to adjust the playrate"), "direction": 0, "interactionMode": 0, "interactionHidden": true, - "loops": 3 + "loops": 0 }] } diff --git a/qml/lib/hints/SlideShowHint.qml b/qml/lib/hints/SlideShowHint.qml index c121957c0c8d586c838c87acce7bfc47abe6e381..ce48eff3c0fd4b5dbe3cefbcca278d93f80fab39 100644 --- a/qml/lib/hints/SlideShowHint.qml +++ b/qml/lib/hints/SlideShowHint.qml @@ -27,7 +27,8 @@ Item { width: Theme.iconSizeExtraLarge height: Theme.iconSizeExtraLarge property int index: slideNr - source: !slideloaded ? "" : slideconfig.centeredComponent + source: (!slideloaded || slideconfig.centeredComponent + === undefined) ? "" : slideconfig.centeredComponent visible: !slideloaded ? false : slideconfig.centeredComponent !== undefined active: !slideloaded ? false : slideconfig.centeredComponent !== undefined onActiveChanged: console.info("loading component " + source) diff --git a/test/test_podcast.py b/test/test_podcast.py index 9531750172ca112727e140252a030a86dd41e011..b581dfb0f825876f5e606d141ab3b3c27e462b75 100644 --- a/test/test_podcast.py +++ b/test/test_podcast.py @@ -2,6 +2,8 @@ Test Podcast functions """ import logging +import os +from io import StringIO from typing import Tuple, List import httpretty @@ -21,6 +23,11 @@ from test import xml_headers, read_testdata logger = logging.getLogger(__name__) +def request_callback(request, uri, response_headers): + with open(os.path.join(os.path.dirname(__file__), "testdata/empty_image.png"), "rb") as file: + return [200, response_headers, file.read()] + + @httpretty.activate(allow_net_connect=False) def test_feed_entry(): """ @@ -35,15 +42,14 @@ def test_feed_entry(): url_fs = 'https://freakshow.fm/feed/opus/' HTTPretty.register_uri(HTTPretty.GET, url_fs, body=read_testdata('testdata/freakshow.rss'), adding_headers=xml_headers) - image = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00}\x00}\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x00\xf7\xfa(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80\n(\xa2\x80?\xff\xd9' HTTPretty.register_uri(HTTPretty.GET, 'https://meta.metaebene.me/media/mm/freakshow-logo-1.0.jpg', - body=image) + body=request_callback) HTTPretty.register_uri(HTTPretty.GET, 'https://elroy.twit.tv/sites/default/files/styles/twit_album_art_144x144/public/images/shows/security_now/album_art/audio/sn_albumart_mask.jpg?itok=VEh3JGKV', - body=image) + body=request_callback) HTTPretty.register_uri(HTTPretty.GET, 'https://elroy.twit.tv/sites/default/files/styles/twit_album_art_2048x2048/public/images/shows/security_now/album_art/audio/sn_albumart_mask.jpg?itok=scC8c-TL', - body=image) + body=request_callback) seen_postids = {} @@ -207,6 +213,7 @@ def test_podcastlist_refresh(refreshable_podcast_fixture): assert plist.get_podcast_count() == 1 assert list(plist.refresh(0)) == [(2, p.title, 'Hello, I am a new fake episode for testing!!öäü', 0)] + @pytest.mark.parametrize("refreshable_podcast_fixture", ["testdata/fakefeed"], indirect=True) def test_move(refreshable_podcast_fixture): p, episodes = refreshable_podcast_fixture @@ -222,7 +229,6 @@ def test_move(refreshable_podcast_fixture): assert len(QueueData.get_queue()) == 1 - @pytest.fixture def refreshable_podcast_fixture(request) -> Tuple[Podcast, List[Podpost]]: """ @@ -247,8 +253,6 @@ def refreshable_podcast_fixture(request) -> Tuple[Podcast, List[Podpost]]: assert invoked > 0 - - @httpretty.activate(allow_net_connect=False) def test_pagination_stops(): url_f1 = "https://fakefeed.com/" diff --git a/test/testdata/empty_image.png b/test/testdata/empty_image.png new file mode 100644 index 0000000000000000000000000000000000000000..f11f13440cc18625138ff5ddb4a5d9937ac56f64 Binary files /dev/null and b/test/testdata/empty_image.png differ diff --git a/translations/harbour-podqast-de.ts b/translations/harbour-podqast-de.ts index c0e4996ee1edd893afbb559e9d8aaf538183cefa..dfc3338510c89287db890e5609a8ffa6c19fd253 100644 --- a/translations/harbour-podqast-de.ts +++ b/translations/harbour-podqast-de.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration diff --git a/translations/harbour-podqast-es.ts b/translations/harbour-podqast-es.ts index c80c63371246b2841c182391cf9e4f9de12a87bc..176a293f61c3612526eafa647ed7d43b24668cd5 100644 --- a/translations/harbour-podqast-es.ts +++ b/translations/harbour-podqast-es.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration diff --git a/translations/harbour-podqast-fr.ts b/translations/harbour-podqast-fr.ts index 0f66804e061f2d9d11cde4366a085144f73d40a7..d19d84510089a31b383a1e911369abd80e61f46b 100644 --- a/translations/harbour-podqast-fr.ts +++ b/translations/harbour-podqast-fr.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration diff --git a/translations/harbour-podqast-sv.ts b/translations/harbour-podqast-sv.ts index 7b652c33e05038fa4aa19f1e11b3862e88482813..65693375b044c1facced95edd396a33ceeff50cf 100644 --- a/translations/harbour-podqast-sv.ts +++ b/translations/harbour-podqast-sv.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration diff --git a/translations/harbour-podqast-zh_CN.ts b/translations/harbour-podqast-zh_CN.ts index 5b494c2195f5dc1c0f4c7ca8ef88655d9c294d8b..64e140442450ed3c0ca6b078cb711734b629573c 100644 --- a/translations/harbour-podqast-zh_CN.ts +++ b/translations/harbour-podqast-zh_CN.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration diff --git a/translations/harbour-podqast.ts b/translations/harbour-podqast.ts index 90be4447db064bc0084b71a483bd61bfdaee53ab..125c00f57e5c76b5c204dc5c5cb7123a35079806 100644 --- a/translations/harbour-podqast.ts +++ b/translations/harbour-podqast.ts @@ -128,7 +128,7 @@ - <b>Whats New?</b><ul><li>Some of you lost episodes in the last migration. Because we found this pretty sad, we're trying to recover them now.</li><li>If you are missing old episodes of feeds in general and this didn't help, try the force refresh button in the settings (We added support for Pagination).</li><li>Podcasts are now sorted alphabetically, we hope you like that!</li><li>Gpodder should work again.</li><li>Check the new Log Page next to the settings.</li><li>If you're streaming an episode and the download finishes, we're now switching to the downloaded file automatically.</li><li>Also more speed, bugfixes and code cleanups.</li></ul><br>If you want to contribute to podQast, you can help translating the app or report issues on GitLab. + whatsnew text whatsnew section of the migration