diff --git a/harbour-podqast.pro b/harbour-podqast.pro index ee9b0d50c515b07544515ee9a0e68cf467bbcda5..89be3d7d1578a8bf14877a73cb927512755f9576 100644 --- a/harbour-podqast.pro +++ b/harbour-podqast.pro @@ -20,13 +20,20 @@ DISTFILES += qml/podqast.qml \ qml/components/FyydDePython.qml \ qml/components/LogHandler.qml \ qml/components/MigrationHandler.qml \ + qml/components/PodqastAudioPlayer.qml \ + lib/hints/SlideShowHint.qml \ + qml/components/hints/HintDockIcons.qml \ + qml/components/hints/HintEpisodeImage.qml \ qml/components/timeutil.js \ qml/cover/CoverPage.qml \ qml/pages/DataMigration.qml \ qml/pages/FyydSearchPage.qml \ qml/pages/GpodderSearchPage.qml \ qml/pages/Log.qml \ + components/hints/MainPageUsageHint.qml \ + components/hints/PlayerHint.qml \ qml/pages/PodcastDirectorySearchPage.qml \ + lib/hints/PodqastSingleTimeHint.qml \ qml/pages/SubscribePodcast.qml \ rpm/podqast.changes.in \ rpm/podqast.changes.run.in \ @@ -63,7 +70,6 @@ DISTFILES += qml/podqast.qml \ qml/components/ArchivePostListItem.qml \ qml/pages/History.qml \ qml/pages/Favorites.qml \ - qml/components/PodSelectGrid.qml \ qml/components/PodcastItem.qml \ qml/pages/PodpostList.qml \ qml/pages/Settings.qml \ @@ -88,6 +94,8 @@ DISTFILES += qml/podqast.qml \ SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 +QML_IMPORT_PATH += "qml/componentspo" + # to disable building translations every time, comment out the # following CONFIG line CONFIG += sailfishapp_i18n diff --git a/python/podcast/archive.py b/python/podcast/archive.py index 98e850eb8f792c3ff4d6a029825edc02145b27c4..2a467844ebebb8c67b6cd2b138e4dd4162c8b7e6 100644 --- a/python/podcast/archive.py +++ b/python/podcast/archive.py @@ -9,6 +9,7 @@ from typing import List, Iterator from peewee import ModelSelect, IntegrityError from podcast import POST_ID_TYPE from podcast.AbstractPodpostListEntry import AbstractPodpostListEntry +from podcast.podcast import PodcastFactory sys.path.append("../") @@ -64,7 +65,7 @@ class Archive: Iterator[Podpost]: query: ModelSelect = ArchiveEntry.select(Podpost).join(Podpost) if url_filter: - query = query.where(Podpost.podurl == url_filter) + query = query.where(Podpost.podcast == PodcastFactory().get_podcast(url_filter)) if filter_favorite: query = query.where(Podpost.favorite == True) if sort_by_insertdate: diff --git a/python/podcast/data_migration.py b/python/podcast/data_migration.py index ef96e048635039cc988fe0d53af5742aabc29a01..f428b23a39c816908d3ac86ffb2df4d1cb78fc1f 100644 --- a/python/podcast/data_migration.py +++ b/python/podcast/data_migration.py @@ -29,12 +29,14 @@ target_data_version = 3 def needs_migration(): def app_is_pristine(): if get_versionnumber() != 0: + logger.debug("app version %s", get_versionnumber()) return False - if os.path.exists(Constants().sqlitepath): - return False - if not os.path.exists(Constants().storepath): - return True - return len(os.listdir(Constants().storepath)) == 0 + if os.path.exists(Constants().storepath): + i = len(os.listdir(Constants().storepath)) + logger.debug("store exists with %d items", i) + return i == 0 + + return True if get_versionnumber() >= target_data_version: logger.info("Dataversion is fine.") @@ -252,11 +254,8 @@ def create_post_from_store_dict(post_dict, podcast): def migrate_podpost_v0_v1(post_dict: dict, podpost: Podpost, do_persist=True): if not podpost: raise ValueError("podpost must not be none") - if hasattr(podpost, "entry") and not podpost.isaudio: - podpost.init(podpost.entry, podpost.logo_url, podpost.podurl) for attr in ["favorite", "file_path", "percentage", "position", "state"]: transfer_attribute(attr, post_dict, podpost) - if do_persist: PodpostFactory().persist(podpost) diff --git a/python/podcast/favorite.py b/python/podcast/favorite.py index c529d23dbe1ff96bfe77d05e3b8aef45352c0187..2cd99c83a3dd634e6d00871b714a6bc8eca3b8ca 100644 --- a/python/podcast/favorite.py +++ b/python/podcast/favorite.py @@ -1,30 +1,6 @@ from podcast.podcast import PodcastFactory from podcast.podpost import Podpost - -def get_queue_favorites(podurl=None): - """ - Return a list of all favorites posts from queue - """ - - from podcast.queue import QueueFactory - - return list(QueueFactory().get_favorites(podurl)) - - - -def append_archive_favorites(posts, podurl=None): - """ - append archive favorites to posts list - """ - - from podcast.archive import ArchiveFactory - - archive = ArchiveFactory().get_archive() - return list(archive.get_podpost_objects(url_filter=podurl,filter_favorite=True)) - - - def get_favorites(podurl=None): query = Podpost.select() if podurl: @@ -33,23 +9,3 @@ def get_favorites(podurl=None): else: query = query.where(Podpost.favorite) return query.order_by(Podpost.insert_date.desc()).iterator() - - -def get_favorite_podcast_stats(): - """ - """ - - entries = {} - - posts = get_queue_favorites() - posts = append_archive_favorites(posts) - for p in posts: - if p.podurl in entries: - entries[p.podurl]["count"] += 1 - else: - if p.logo_url: - logo_url = p.logo_url - else: - logo_url = "../../images/podcast.png" - entries[p.podurl] = {"count": 1, "logo_url": logo_url} - return entries diff --git a/python/podcast/feedutils.py b/python/podcast/feedutils.py index 2f5485a26f21e1492eeee24dfd0eab59c7017e1e..89624420e8c508cf2b3927474c4df6c9d7e2ee82 100644 --- a/python/podcast/feedutils.py +++ b/python/podcast/feedutils.py @@ -1,6 +1,6 @@ import time import logging -from typing import Iterator +from typing import Iterator, Callable import feedparser from feedparser import FeedParserDict @@ -34,30 +34,33 @@ def fetch_feed(published, url) -> FeedParserDict: if feed.status == 304: raise NotModified() if feed.bozo != 0: - exc : Exception = feed.bozo_exception + exc: Exception = feed.bozo_exception if type(exc) != feedparser.CharacterEncodingOverride: logger.exception( "Podcast init: error in parsing feed %s", str(type(exc)), exc_info=exc ) raise FeedFetchingError(exc.message if hasattr(exc, 'message') else "message_missing") + if "itunes_new-feed-url" in feed.feed and feed.feed["itunes_new-feed-url"] != url: + raise NewFeedUrlError(feed.feed["itunes_new-feed-url"]) logger.info( "podcast init size of entries: %d", len(feed.entries) ) return feed -def iterate_feed_entries(feed) -> Iterator: +def iterate_feed_entries(feed, should_fetch_next_page: Callable[[], bool] = lambda: True) -> Iterator: while True: - for entry in feed.entries: + for entry in sorted(feed.entries, key=lambda e: e.published_parsed, reverse=True): yield entry - next_page_href = get_next_page(feed) - if next_page_href != None: - logger.info("Found next page: %s", next_page_href) - feed = fetch_feed(0, next_page_href) - if feed is None: + if should_fetch_next_page(): + next_page_href = get_next_page(feed) + if next_page_href != None: + logger.info("Found next page: %s", next_page_href) + feed = fetch_feed(0, next_page_href) + if feed is None: + break + else: break - else: - break class FeedFetchingError(BaseException): @@ -67,3 +70,8 @@ class FeedFetchingError(BaseException): class NotModified(Exception): pass + + +class NewFeedUrlError(BaseException): + def __init__(self, url): + self.url = url diff --git a/python/podcast/inbox.py b/python/podcast/inbox.py index fbd39966eee7c8c25eb939646469725e584b712f..5f73a11b44132e36cec3f40e3711266ed9dc4ed3 100644 --- a/python/podcast/inbox.py +++ b/python/podcast/inbox.py @@ -8,6 +8,7 @@ from typing import List, Iterator from peewee import ModelSelect from podcast import POST_ID_TYPE from podcast.AbstractPodpostListEntry import AbstractPodpostListEntry +from podcast.podcast import Podcast from podcast.podpost import Podpost sys.path.append("../") @@ -58,7 +59,7 @@ class Inbox: def get_podposts_objects(self, podurl=None) -> Iterator[Podpost]: query: ModelSelect = self.__select_inbox_entries(Podpost) if podurl: - query = query.where(Podpost.podurl == podurl) + query = query.join(Podcast).where(Podpost.podcast.url == podurl) for e in query: yield e.podpost diff --git a/python/podcast/persistent_log.py b/python/podcast/persistent_log.py index 60508d5e91c949b2b73e9867ff6cc9d62654f634..ace1590179f6d879e63db9e2a634bb9a2b9aa274 100644 --- a/python/podcast/persistent_log.py +++ b/python/podcast/persistent_log.py @@ -18,6 +18,7 @@ class LogType(Enum): AddPodcastError = "AddPodcastError" SuccessfulRefresh = "SuccessfulRefresh" Refresh304 = "Refresh304" + FeedRedirect = "FeedRedirect" class LogMessage(BaseModel): diff --git a/python/podcast/podcast.py b/python/podcast/podcast.py index 78da8d293035416e6078918ff5d55fdd53c5e759..645d28308a78aae58550148b8ec9708d0749b00d 100644 --- a/python/podcast/podcast.py +++ b/python/podcast/podcast.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from podcast.constants import Constants, BaseModel from podcast.factory import BaseFactory -from podcast.feedutils import fetch_feed, FeedFetchingError, NotModified +from podcast.feedutils import fetch_feed, FeedFetchingError, NotModified, NewFeedUrlError from podcast.cache import Cache from podcast import util, feedutils @@ -83,6 +83,10 @@ class Podcast(BaseModel): feedCache.store(url, feed) except NotModified: raise FeedFetchingError("Should not get 304 on initial fetching") + except NewFeedUrlError as e: + persist_log(LogType.FeedRedirect, msg="permanently redirected feed", + oldurl=url, newurl=e.url) + return cls.create_from_url(e.url, preview, num_preview_items) episodes = cls.podcast_from_feed(feed, podcast, preview, url, num_preview_items) return podcast, episodes except ValueError: @@ -147,7 +151,7 @@ class Podcast(BaseModel): href_known = "href" in entry and entry.href in known_hrefs has_guid = "guid" in entry guid_known = has_guid and entry["guid"] in known_guids - #logger.debug("entry knowncheck: %s %s %s", has_guid, guid_known, href_known) + # logger.debug("entry knowncheck: %s %s %s", has_guid, guid_known, href_known) return guid_known or href_known if self.autolimit: @@ -165,9 +169,9 @@ class Podcast(BaseModel): continue try: post = Podpost.from_feed_entry(self, entry, self.url, image) - except: + except Exception as e: logger.exception("could not process entry of podcast %s", self.url) - persist_log(LogType.FeedEpisodeParseError, title=self.title, entry=entry) + persist_log(LogType.FeedEpisodeParseError, title=self.title, entry=entry, exception=e) continue episode_count += 1 new_posts.append(post) @@ -179,8 +183,6 @@ class Podcast(BaseModel): if not do_not_store: from podcast.podpost import PodpostFactory PodpostFactory().persist_bulk(new_posts) - new_post_ids = [post.id for post in new_posts] - new_post_ids.reverse() return new_posts @property @@ -224,10 +226,12 @@ class Podcast(BaseModel): return { "url": self.url, + "link": self.link, "title": title, "description": description, "descriptionfull": descriptionfull, - "logo_url": self.logo() + "logo_url": self.logo(), + "episodecount": self.episodes.count() } def _set_alt_feeds(self, feed): @@ -321,6 +325,7 @@ class Podcast(BaseModel): pyotherside.send("refreshPost", self.title) try: feed = fetch_feed(self.published if not full_refresh else 0, self.url) + self.__set_data_from_feed(feed) PodcastFactory().persist(self) @@ -340,6 +345,12 @@ class Podcast(BaseModel): except FeedFetchingError as ffe: logger.exception("Could not fetch feed") persist_log(LogType.FeedParseError, title=self.title, errormsg=ffe) + except NewFeedUrlError as e: + self.url = e.url + PodcastFactory().persist(self) + persist_log(LogType.FeedRedirect, title=self.title, msg="permanently redirected feed", oldurl=self.url, + newurl=e.url) + yield from self.refresh(moveto, limit, full_refresh) except Exception as e: persist_log(LogType.Exception, msg="during refresh", podcasttitle=self.title, exception=e) diff --git a/python/podcast/podpost.py b/python/podcast/podpost.py index 026e7038585219cddb41cddbd703bb6c61d5c120..24263dbcc685c9c753a2f791e66aa46b8cac1425 100644 --- a/python/podcast/podpost.py +++ b/python/podcast/podpost.py @@ -57,22 +57,20 @@ class Podpost(BaseModel): isaudio: bool = BooleanField(default=False, help_text="Is episode from external folder source") # filesize length: int = IntegerField(default=0) - listened: bool = BooleanField(default=False, help_text="Has been played already") + listened: bool = BooleanField(default=False, null=False, help_text="Has been played already") link: str = TextField(default="") logo_path: str = TextField(null=True) logo_url: str = TextField(null=True) # download percentage percentage: float = FloatField(default=0) - plainpart: str = TextField(default="") - # the feed's url - podurl: str = TextField(null=True) + plainpart: TextField = TextField(default="") position: int = IntegerField(default=0) podcast = ForeignKeyField(Podcast, null=True, backref='episodes', lazy_load=True, on_delete='CASCADE') # when the post was published according to feed published = DateTimeField() # play_state state: int = IntegerField(default=STOP) - title: str = TextField() + title: TextField = TextField() # mimetype type: str = TextField(null=True) @@ -97,7 +95,6 @@ class Podpost(BaseModel): post = cls() post.podcast = podcast post.logo_url = logo_url - post.podurl = podurl if logo_url and len(logo_url) > 0 and logo_url[0] == "/": post.logo_path = logo_url else: @@ -203,7 +200,6 @@ class Podpost(BaseModel): post.length = int(filesize) post.published = 0 post.type = "audio/" + tech["filetype"] - post.podurl = afile post.isaudio = True return post @@ -237,7 +233,8 @@ class Podpost(BaseModel): return { "id": self.id, "title": self.title, - "url": self.podurl, + "podcast_url": self.podcast.url, + "audio_url": self.href, "logo_url": image, "link": self.link, "description": self.plainpart, @@ -385,8 +382,12 @@ class Podpost(BaseModel): """ self.position = position + if self.position > 0 and ( + self.duration * 1000 - self.position < Constants().markListenedBeforeEndThreshold * 1000): + self.listened = True PodpostFactory().persist(self) + @property def get_position(self): """ @@ -595,7 +596,7 @@ class PodpostFactory(BaseFactory): from podcast.podcast import Podcast if (type(podcast) != Podcast): raise ValueError("supplied argument must be a podcast") - query: ModelSelect = Podpost.select().where(Podpost.podurl == podcast.url).order_by(Podpost.published.desc()) + query: ModelSelect = Podpost.select().where(Podpost.podcast == podcast).order_by(Podpost.published.desc()) if limit > 0: query = query.limit(limit) for post in query.iterator(): diff --git a/python/podcast/queue.py b/python/podcast/queue.py index bafec0216b1210b2f7626a2c9586422b90824d36..ae33bbc493654137262240fdf2f6f7813bfb39bd 100644 --- a/python/podcast/queue.py +++ b/python/podcast/queue.py @@ -263,6 +263,7 @@ class Queue: self.podposts.remove(podpost) self.podposts.insert(ind - 1, podpost) self.save() + return ind - 1 @sanitize_podpost def move_down(self, podpost): @@ -373,13 +374,4 @@ class QueueFactory(metaclass=Singleton): logger.warning("Podpost %s is in queue but not found", postid) def get_first_podpost_object(self): - return next(self.get_podposts_objects(), None) - - def get_favorites(self, podurl) -> Iterator[Podpost]: - for post in self.get_podposts_objects(): - if post.favorite: - if podurl: - if post.podurl == podurl: - yield post - else: - yield post + return next(self.get_podposts_objects(), None) \ No newline at end of file diff --git a/python/podcast/search/DictParser.py b/python/podcast/search/DictParser.py new file mode 100644 index 0000000000000000000000000000000000000000..5e1a5f47958507b4e8ba01fd3554368f2d6c5228 --- /dev/null +++ b/python/podcast/search/DictParser.py @@ -0,0 +1,62 @@ +from typing import Dict, Iterator + +from podcast.podcast import PodcastFactory +from podcast.search.Filter import AndFilter, PodpostTextFilter, FavoriteFilter, PodpostListenedFilter, Filter, \ + PodcastFilter +from podcast.search.Order import Order, PodpostPublishedDateOrder, Direction, PodpostListInsertDateOrder +from podcast.search.SearchBase import AllSearchBase + + +def iterator_from_searchoptions(searchoptions: Dict) -> Iterator[Dict]: + podcast_cache = {} + if "podcast_url" in searchoptions: + podcast_cache[searchoptions["podcast_url"]] = PodcastFactory().get_podcast(searchoptions["podcast_url"]) + + filter = create_filter_from_options(searchoptions, podcast_cache) + order = create_order_from_options(searchoptions) + return (post.get_data() for post in AllSearchBase().iter_search(filter, order)) + + + +def create_filter_from_options(searchoptions: Dict, podcast_cache: Dict = None) -> Filter: + if podcast_cache is None: + podcast_cache = {} + + def and_add_filter(old, new): + if old is None: + return new + if new is None: + return old + return AndFilter(old, new) + + filter = None + if "searchtext" in searchoptions: + searchtext = searchoptions["searchtext"] + filter = and_add_filter(filter, PodpostTextFilter(searchtext)) + if "fav_filter" in searchoptions: + filter = and_add_filter(filter, FavoriteFilter(searchoptions["fav_filter"])) + if "listened_filter" in searchoptions: + filter = and_add_filter(filter, PodpostListenedFilter(searchoptions["listened_filter"])) + if "podcast_url" in searchoptions: + url = searchoptions["podcast_url"] + if url not in podcast_cache: + raise AttributeError("podcast filter defined but podcast not cached") + filter: Filter = and_add_filter(filter, PodcastFilter(podcast_cache[url])) + if "podcast_id" in searchoptions: + filter: Filter = and_add_filter(filter, PodcastFilter(searchoptions["podcast_id"])) + return filter + + +def create_order_from_options(searchoptions: Dict) -> Order: + dir = Direction.DESC + if "order_dir" in searchoptions: + if searchoptions["order_dir"] == "asc": + dir = Direction.ASC + order: Order = PodpostPublishedDateOrder(dir) + + if "order" in searchoptions: + if searchoptions["order"] == "published": + order = PodpostPublishedDateOrder(dir) + if searchoptions["order"] == "inserted": + order = PodpostListInsertDateOrder(dir) + return order diff --git a/python/podcast/search/Filter.py b/python/podcast/search/Filter.py new file mode 100644 index 0000000000000000000000000000000000000000..5cee56263c293e40a949c1d2b0aae8e71edb131c --- /dev/null +++ b/python/podcast/search/Filter.py @@ -0,0 +1,94 @@ +from abc import abstractmethod, ABC +from typing import Dict + +from peewee import ModelSelect, Expression +from podcast.podcast import Podcast +from podcast.podpost import Podpost + + +class Filter(ABC): + def apply_filter(self, query: ModelSelect, search_env: Dict) -> ModelSelect: + return query.where(self.get_where_expression(search_env)) + + @abstractmethod + def get_where_expression(self, search_env: Dict) -> Expression: + raise ValueError("Do not call abstract method") + + + def __eq__(self, other): + if other is None: + return False + if other.__class__ == self.__class__: + return self.__dict__ == other.__dict__ + else: + return False + + +class NoFilter(Filter): + """ + Warning: do not use as part of other filters + """ + + def apply_filter(self, query: ModelSelect, _) -> ModelSelect: + return query + + def get_where_expression(self, search_env: Dict) -> Expression: + raise NotImplementedError() + + +class PodcastFilter(Filter): + def __init__(self, podcast: Podcast) -> None: + super().__init__() + self.podcast = podcast + + def get_where_expression(self, search_env: Dict) -> Expression: + return Podpost.podcast == self.podcast + + +class PodpostTextFilter(Filter): + + def __init__(self, searchstring: str) -> None: + super().__init__() + self.searchstring = searchstring + + def get_where_expression(self, search_env: Dict) -> Expression: + return Podpost.title.contains(self.searchstring) or Podpost.plainpart.contains(self.searchstring) + + +class PodpostListenedFilter(Filter): + def __init__(self, filterval) -> None: + super().__init__() + self.filterval = filterval + + def get_where_expression(self, search_env: Dict) -> Expression: + return Podpost.listened == self.filterval + + +class FavoriteFilter(Filter): + def __init__(self,filterval) -> None: + super().__init__() + self.filterval = filterval + + def get_where_expression(self, search_env: Dict) -> Expression: + return Podpost.favorite == self.filterval + + +class BiFilter(Filter): + def __init__(self, left: Filter, right: Filter) -> None: + super().__init__() + self.left = left + self.right = right + + @abstractmethod + def get_where_expression(self, search_env: Dict) -> Expression: + raise ValueError("Do not call abstract method") + + +class AndFilter(BiFilter): + def get_where_expression(self, search_env: Dict) -> Expression: + return self.left.get_where_expression(search_env) & self.right.get_where_expression(search_env) + + +class OrFilter(BiFilter): + def get_where_expression(self, search_env: Dict) -> Expression: + return self.left.get_where_expression(search_env) | self.right.get_where_expression(search_env) diff --git a/python/podcast/search/Order.py b/python/podcast/search/Order.py new file mode 100644 index 0000000000000000000000000000000000000000..b21e7d0ff9faf3f9d4951dff9f5911c6d54e11ec --- /dev/null +++ b/python/podcast/search/Order.py @@ -0,0 +1,62 @@ +from abc import abstractmethod, ABC +from enum import Enum, auto +from typing import Dict + +from peewee import ModelSelect +from podcast.podpost import Podpost + + +class Direction(Enum): + ASC = auto() + DESC = auto() + + +def handle_order(expression, order: Direction): + if order == Direction.ASC: + return expression.asc() + elif order == Direction.DESC: + return expression.desc() + else: + raise ValueError("Invalid Direction: " + str(order)) + + +class Order(ABC): + dir: Direction + + def __init__(self, dir: Direction) -> None: + self.dir = dir + + @abstractmethod + def apply_order(self, query: ModelSelect, search_env: Dict) -> ModelSelect: + raise ValueError("do not call abstract method") + + def __eq__(self, other): + if other is None: + return False + if other.__class__ == self.__class__: + return self.__dict__ == other.__dict__ + else: + return False + + def __str__(self) -> str: + return self.__class__.__name__ + "("+str(self.__dict__)+")" + + +class NoOrder(Order): + def __init__(self) -> None: + super().__init__(Direction.ASC) + + def apply_order(self, query: ModelSelect, _) -> ModelSelect: + return query + + +class PodpostListInsertDateOrder(Order): + def apply_order(self, query: ModelSelect, search_env: Dict) -> ModelSelect: + if not "list_type" in search_env: + raise ValueError("listtype must be set when ordering by it") + return query.order_by(handle_order(search_env["list_type"].insert_date, self.dir)) + + +class PodpostPublishedDateOrder(Order): + def apply_order(self, query: ModelSelect, search_env: Dict) -> ModelSelect: + return query.order_by(handle_order(Podpost.published, self.dir)) diff --git a/python/podcast/search/SearchBase.py b/python/podcast/search/SearchBase.py new file mode 100644 index 0000000000000000000000000000000000000000..27d96a390fac8d0f8b8b01b2ecacbafb01aad6b3 --- /dev/null +++ b/python/podcast/search/SearchBase.py @@ -0,0 +1,67 @@ +import logging +from abc import abstractmethod, ABC + +from peewee import ModelSelect +from podcast.inbox import InboxEntry +from podcast.podpost import Podpost +from podcast.search.Order import Order +from podcast.search.Filter import Filter + +logger = logging.getLogger(__name__) + + +class SearchBase(ABC): + + def __init__(self) -> None: + self.search_env = {} + + @abstractmethod + def get_search_base(self) -> ModelSelect: + raise ValueError("do not call abstract method") + + @abstractmethod + def unpack_result_to_podpost(self, result) -> Podpost: + raise ValueError("do not call abstract method") + + # -> Iterator[Podpost] + def iter_search(self, filter: Filter, order: Order): + query = self.get_search_base() + if filter is not None: + query = filter.apply_filter(query, self.search_env) + else: + logger.warning("no filter set") + if order is not None: + query = order.apply_order(query, self.search_env) + else: + logger.warning("no order set") + logger.debug(query) + for result in query.iterator(): + yield self.unpack_result_to_podpost(result) + + +class AllSearchBase(SearchBase): + def get_search_base(self) -> ModelSelect: + return Podpost.select() + + def unpack_result_to_podpost(self, result) -> Podpost: + return result + + +class FavoriteSearchBase(SearchBase): + def get_search_base(self) -> ModelSelect: + return Podpost.select().where(Podpost.favorite == True) + + def unpack_result_to_podpost(self, result) -> Podpost: + return result + + +class InboxSearchBase(SearchBase): + def __init__(self) -> None: + super().__init__() + self.search_env['list_type'] = InboxEntry + + def get_search_base(self) -> ModelSelect: + return InboxEntry.select(Podpost).join(Podpost) + + def unpack_result_to_podpost(self, result) -> Podpost: + return result.podpost diff --git a/python/podcast/search/__init__.py b/python/podcast/search/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/qml/components/ArchiveHandler.py b/qml/components/ArchiveHandler.py index 4b0f2a02154263d6eb9d820de11b760ee28e5d28..f66bb1582c06ddc7b265c6548cd23d7dd2baac4f 100644 --- a/qml/components/ArchiveHandler.py +++ b/qml/components/ArchiveHandler.py @@ -22,34 +22,10 @@ async def get_archive_posts(podurl=None): pyotherside.send("historyData", offset, [post.get_data() for post in chunk]) -async def get_archive_pod_data(): - """ - """ - - entries = {} - - archive = ArchiveFactory().get_archive() - - for entry in archive.get_podpost_objects(): - if entry.podurl in entries: - entries[entry.podurl]["count"] += 1 - else: - if entry.logo_url: - logo_url = entry.logo_url - else: - logo_url = "../../images/podcast.png" - entries[entry.podurl] = {"count": 1, "logo_url": logo_url} - - pyotherside.send("archivePodList", entries) - - class ArchiveHandler: def getarchiveposts(self, podurl=None): asyncio.run(get_archive_posts(podurl)) - def getarchivepoddata(self): - asyncio.run(get_archive_pod_data()) - archivehandler = ArchiveHandler() diff --git a/qml/components/ArchiveHandlerPython.qml b/qml/components/ArchiveHandlerPython.qml index b2d5f43a9e8fd90a081e5ecdd88a32f1528c4600..94dcf9e2f672e1131fa94d6abf6f9f09ebf14d87 100644 --- a/qml/components/ArchiveHandlerPython.qml +++ b/qml/components/ArchiveHandlerPython.qml @@ -6,12 +6,10 @@ Python { id: archivehandler signal historyData(int offset, var data) - signal archivePodList(var data) Component.onCompleted: { addImportPath(Qt.resolvedUrl("./python")) setHandler("historyData", historyData) - setHandler("archivePodList", archivePodList) addImportPath(Qt.resolvedUrl('.')) importModule('ArchiveHandler', function () { @@ -32,8 +30,4 @@ Python { function () {}) } } - - function getArchivePodData() { - call("ArchiveHandler.archivehandler.getarchivepoddata", function () {}) - } } diff --git a/qml/components/EpisodeImage.qml b/qml/components/EpisodeImage.qml index d804b5ea1efd58b0a4338b4cf58d9757215460f5..82d51e8dc7326fbfbb122ae42e6733236de2b34a 100644 --- a/qml/components/EpisodeImage.qml +++ b/qml/components/EpisodeImage.qml @@ -4,14 +4,13 @@ import Nemo.Thumbnailer 1.0 Thumbnail { id: listicon - property int percentage: dlperc + property int percentage anchors.leftMargin: Theme.paddingMedium - property bool showDlIcon: listicon.percentage > 0 - && listicon.percentage < 100 - property bool showIsAudio: isaudio - property bool showIsPlaying: id === playerHandler.firstid - property bool showIsListened: listened + property bool showDownladingState: percentage > 0 && percentage < 100 + property bool showIsAudio + property bool showIsPlaying + property bool showIsListened MouseArea { anchors.fill: parent @@ -21,16 +20,14 @@ Thumbnail { }) } } - anchors.left: parent.left width: Theme.iconSizeLarge height: Theme.iconSizeLarge sourceSize.width: Theme.iconSizeLarge sourceSize.height: Theme.iconSizeLarge - anchors.verticalCenter: parent.verticalCenter source: logo_url == "" ? "../../images/podcast.png" : logo_url Rectangle { id: dlstatus - visible: showDlIcon + visible: showDownladingState anchors.right: parent.right height: parent.height width: (100 - listicon.percentage) * parent.width / 100 diff --git a/qml/components/EpisodeSearchComponent.qml b/qml/components/EpisodeSearchComponent.qml new file mode 100644 index 0000000000000000000000000000000000000000..85aaddaf8b30f1ae53bd892b0651e7307416acf2 --- /dev/null +++ b/qml/components/EpisodeSearchComponent.qml @@ -0,0 +1,76 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../components" + +Column { + + SearchField { + id: episodeSearchField + width: parent.width + height: Theme.itemSizeMedium + visible: false + } + + Row { + id: episodeSortField + width: parent.width + visible: false + height: Theme.itemSizeMedium + LayoutMirroring.enabled: true + + IconButton { + icon.source: "image://theme/icon-s-duration" + } + + IconButton { + icon.source: "image://theme/icon-m-moon" + } + + IconButton { + icon.source: "image://theme/icon-m-mic-mute" + } + } + + Row { + id: settingsrow + height: Theme.itemSizeMedium + width: parent.width + visible: false + LayoutMirroring.enabled: true + + IconButton { + icon.source: "image://theme/icon-m-cancel" + onClicked: { + settingsrow.visible = false + } + } + + IconButton { + id: prefsicon + icon.source: "image://theme/icon-m-developer-mode" + onClicked: { + pageStack.push(Qt.resolvedUrl( + "PodcastSettings.qml"), { + "podtitle": title, + "url": url + }) + } + } + + IconButton { + id: searchicon + icon.source: "image://theme/icon-m-search" + onClicked: { + episodeSearchField.visible = !episodeSearchField.visible + } + } + + IconButton { + id: sorticon + icon.source: "image://theme/icon-m-transfer" + onClicked: { + episodeSortField.visible = !episodeSortField.visible + } + } + } +} diff --git a/qml/components/ExternalHandler.py b/qml/components/ExternalHandler.py index 7ba3c1e93dd041f0e5e17f076511b451eb2f635d..fefafee1e801eefb827819c6c745d7e584e45c5a 100644 --- a/qml/components/ExternalHandler.py +++ b/qml/components/ExternalHandler.py @@ -139,26 +139,6 @@ def get_external_posts(): pyotherside.send("createExternalList", entries) -def get_external_pod_data(): - """ - """ - - entries = {} - - external = ExternalFactory().get_external() - - for filename, entry in external.get_podposts_objects(): - if entry.podurl in entries: - entries[entry.podurl]["count"] += 1 - else: - if entry.logo_url: - logo_url = entry.logo_url - else: - logo_url = "../../images/podcast.png" - entries[entry.podurl] = {"count": 1, "logo_url": logo_url} - - pyotherside.send("archivePodList", entries) - def get_audio_data(afile): """ @@ -189,12 +169,6 @@ class ExternalHandler: self.bgthread2 = threading.Thread(target=get_external_posts) self.bgthread2.start() - def getexternalpoddata(self): - if self.bgthread.is_alive(): - return - self.bgthread = threading.Thread(target=get_external_pod_data) - self.bgthread.start() - def waitnew(self): if self.inothread.is_alive(): return diff --git a/qml/components/ExternalHandlerPython.qml b/qml/components/ExternalHandlerPython.qml index 6dda009925de3adcd75402891f0308e283717207..54161d9f90a4ecaa6e7065095c633a1227863bd0 100644 --- a/qml/components/ExternalHandlerPython.qml +++ b/qml/components/ExternalHandlerPython.qml @@ -6,46 +6,37 @@ Python { id: externalhandler signal createExternalList(var data) - signal externalPodList(var data) - signal externalUpdated() - signal objectLoaded() + signal externalUpdated + signal objectLoaded signal audioInfo(string info) Component.onCompleted: { setHandler("createExternalList", createExternalList) - setHandler("externalPodList", externalPodList) setHandler("externalUpdated", externalUpdated) setHandler("objectLoaded", objectLoaded) setHandler("audioInfo", audioInfo) - addImportPath(Qt.resolvedUrl('.')); + addImportPath(Qt.resolvedUrl('.')) importModule('ExternalHandler', function () { console.log('ExternalHandler is now imported') }) } function getExternalEntries() { - // call("ExternalHandler.externalhandler.getexternalposts", function() {}); - call("ExternalHandler.get_external_posts", function() {}); + call("ExternalHandler.get_external_posts", function () {}) } -// function getExternalPodData() { -// call("ExternalHandler.externalhandler.getexternalpoddata", function() {}); -// // call("ExternalHandler.get_external_pod_data", function() {}); -// } function waitNew() { - call("ExternalHandler.externalhandler.waitnew", function() {}); - // call("ExternalHandler.wait_new", function() {}); + call("ExternalHandler.externalhandler.waitnew", function () {}) } function checkNew() { - call("ExternalHandler.check_new", function() {}); + call("ExternalHandler.check_new", function () {}) } function getAudioData(audio_file) { - call("ExternalHandler.get_audio_data", [audio_file], function() {}) + call("ExternalHandler.get_audio_data", [audio_file], function () {}) } onError: { - console.log('python error: ' + traceback); + console.log('python error: ' + traceback) } - } diff --git a/qml/components/FavoriteHandler.py b/qml/components/FavoriteHandler.py index 34cab37567ae832cbee0f9c3f0e784e9d415005c..1cde4fd2a049e660961af2273a0637da4f1e7b81 100644 --- a/qml/components/FavoriteHandler.py +++ b/qml/components/FavoriteHandler.py @@ -32,10 +32,6 @@ def send_favorites(podurl=None): for offset, max, chunk in chunks(posts, 32): pyotherside.send("favoriteListData", offset, [post.get_data() for post in chunk]) -def get_fav_pod_data(): - pyotherside.send("archivePodList", favorite.get_favorite_podcast_stats()) - - class FavoriteHandler: def __init__(self): self.bgthread = threading.Thread() @@ -62,11 +58,5 @@ class FavoriteHandler: self.bgthread2 = threading.Thread(target=send_favorites) self.bgthread2.start() - def getfavpoddata(self): - if self.bgthread.is_alive(): - return - self.bgthread = threading.Thread(target=get_fav_pod_data) - self.bgthread.start() - favoritehandler = FavoriteHandler() diff --git a/qml/components/FavoriteHandlerPython.qml b/qml/components/FavoriteHandlerPython.qml index 98f07382cd52e06c559d0f6b37b269bc5d035d7a..ec8902b53e33c47dd02f62071c1ade4c2692df77 100644 --- a/qml/components/FavoriteHandlerPython.qml +++ b/qml/components/FavoriteHandlerPython.qml @@ -29,7 +29,4 @@ Python { } } - function getFavPodData() { - call("FavoriteHandler.favoritehandler.getfavpoddata", function() {}); - } } diff --git a/qml/components/FeedParser.py b/qml/components/FeedParser.py index c38dc759d385fe31ddfe478ab8e814017e256e9a..8c58769c3da3bfec99ab5641f4061fbdbab1025a 100644 --- a/qml/components/FeedParser.py +++ b/qml/components/FeedParser.py @@ -16,6 +16,7 @@ from podcast.persistent_log import LogType from podcast.podcast import PodcastFactory, Podcast from podcast.podcastlist import PodcastListFactory from podcast.archive import ArchiveFactory +from podcast.search import DictParser from podcast.util import create_opml, movePost, chunks logger = logging.getLogger(__name__) @@ -45,7 +46,8 @@ class FeedParser: if preview: podcast, episodes = Podcast.create_from_url(url, preview=True, num_preview_items=num_preview_episodes) else: - podcast, episodes = PodcastFactory().get_podcast(url) + podcast = PodcastFactory().get_podcast(url) + episodes = list(podcast.episodes.limit(num_preview_episodes)) if num_preview_episodes > 0 else [] except URLError: pyotherside.send("feedFetchError", url) return @@ -65,17 +67,17 @@ class FeedParser: async def get_podcast_preview(self, theurl, num_preview_episodes): await self.get_feedinfo(theurl, True, num_preview_episodes=num_preview_episodes) - async def get_entries(self, url): + async def get_entries(self, searchoptions): """ Get all podposts """ - - podcast = PodcastFactory().get_podcast(url) - - link = podcast.link - - for offset, max, chunk in chunks(podcast.get_entries(), 32): - pyotherside.send("episodeListData", link, podcast.title, chunk, offset, max) + logger.info("searching with: %s", searchoptions) + triggered = False + for offset, max, chunk in chunks(DictParser.iterator_from_searchoptions(searchoptions), 32): + triggered = True + pyotherside.send("episodeListData",chunk, offset, max) + if not triggered: + pyotherside.send("episodeListData", [], 0, 0) async def subscribe_podcast(self, url): """ @@ -146,15 +148,16 @@ class FeedParser: podcast_list = PodcastListFactory().get_podcast_list() - for postid, podcasttitle, posttitle, move in podcast_list.refwresh(moveto, limit, full_refresh=full_refresh): + for postid, podcasttitle, posttitle, move in podcast_list.refresh(moveto, limit, + full_refresh=full_refresh): try: page = movePost(move, postid) pyotherside.send( "updatesNotification", podcasttitle, posttitle, page ) except Exception as e: - persistent_log.persist_log(LogType.Exception,msg="Could not move episode to according list",exception=e) - + persistent_log.persist_log(LogType.Exception, msg="Could not move episode to according list", + exception=e) pyotherside.send("refreshFinished") async def get_podcast_params(self, url): diff --git a/qml/components/FeedParserPython.qml b/qml/components/FeedParserPython.qml index f0ab84ce7f3cf8fe78ca60ba4d0d6eb8107fdfae..d98a9cb434b585d69cad0d6d772f202c7b8b6c9e 100644 --- a/qml/components/FeedParserPython.qml +++ b/qml/components/FeedParserPython.qml @@ -19,7 +19,7 @@ Python { signal refreshlimit(string podcasttitle) signal backupDone(string tarpath) signal opmlSaveDone(string opmlpath) - signal episodeListData(string podlink, string podtitle, var episodes, string offset, string totalcount) + signal episodeListData(var episodes, string offset, string totalcount) signal feedFetchError(string pod_url) Component.onCompleted: { @@ -61,9 +61,9 @@ Python { call("FeedParser.instance.get_podcasts", function () {}) } - function getEntries(url) { - console.log("url: " + url) - call("FeedParser.instance.get_entries", [url], function () {}) + function getEntries(search_options) { + call("FeedParser.instance.get_entries", [search_options], + function () {}) } function subscribePodcast(url) { diff --git a/qml/components/InboxHandler.py b/qml/components/InboxHandler.py index c9ee361e5527685d33088455ffa14dcabb59609a..b5de1c5d45be4b6749207947c63231d5d87f6ec8 100644 --- a/qml/components/InboxHandler.py +++ b/qml/components/InboxHandler.py @@ -23,27 +23,6 @@ def get_inbox_posts(podurl=None): pyotherside.send("inboxData", offset, [post.get_data() for post in chunk]) -def get_inbox_pod_data(): - """ - """ - - entries = {} - - inbox = InboxFactory().get_inbox() - - for entry in inbox.get_podposts_objects(): - if entry.podurl in entries: - entries[entry.podurl]["count"] += 1 - else: - if entry.logo_url: - logo_url = entry.logo_url - else: - logo_url = "../../images/podcast.png" - entries[entry.podurl] = {"count": 1, "logo_url": logo_url} - - pyotherside.send("archivePodList", entries) - - def move_queue_top(podpost): """ Move element to top of queue @@ -117,12 +96,6 @@ class InboxHandler: self.bgthread = threading.Thread(target=get_inbox_posts) self.bgthread.start() - def getinboxpoddata(self): - if self.bgthread.is_alive(): - return - self.bgthread = threading.Thread(target=get_inbox_pod_data) - self.bgthread.start() - def queuedownload(self, id): """ download audio post diff --git a/qml/components/InboxHandlerPython.qml b/qml/components/InboxHandlerPython.qml index 8fc130a595128e5b31f6fe6f3cff35e351acf164..448c1182b8d56c46fdb204ccbd8f610247330334 100644 --- a/qml/components/InboxHandlerPython.qml +++ b/qml/components/InboxHandlerPython.qml @@ -44,7 +44,4 @@ Python { function moveAllArchive(podpost) { call("InboxHandler.move_all_archive", function () {}) } - function getInboxPodData() { - call("InboxHandler.get_inbox_pod_data", function () {}) - } } diff --git a/qml/components/PlayDockedPanel.qml b/qml/components/PlayDockedPanel.qml index 142e1d1232c2945e1bb4e096ec3a7e47a6ac9a39..e3365702602c03300925ed4c0f689358c56981ba 100644 --- a/qml/components/PlayDockedPanel.qml +++ b/qml/components/PlayDockedPanel.qml @@ -6,9 +6,10 @@ DockedPanel { width: parent.width height: Theme.itemSizeSmall - open: playeropen + open: true dock: Dock.Bottom + default property alias contents: additions.children Rectangle { anchors.fill: parent @@ -17,69 +18,92 @@ DockedPanel { } Row { - id: row anchors.centerIn: parent + Row { + id: row - IconButton { - icon.source: "image://theme/icon-m-previous" - onClicked: { - podqast.fast_backward() + //anchors.centerIn: parent + IconButton { + icon.source: "image://theme/icon-m-previous" + onClicked: { + playerHandler.fast_backward() + } } - } - IconButton { - icon.source: "image://theme/icon-m-" + (playerHandler.isPlaying ? "pause" : "play") - onClicked: { - playerHandler.playpause() + IconButton { + icon.source: "image://theme/icon-m-" + (playerHandler.isPlaying ? "pause" : "play") + onClicked: { + playerHandler.playpause() + } } - } - IconButton { - icon.source: "image://theme/icon-m-next" - onClicked: { - podqast.fast_forward() + IconButton { + icon.source: "image://theme/icon-m-next" + onClicked: { + playerHandler.fast_forward() + } } - } - IconButton { - id: playpanelicon - icon.source: playerHandler.playicon - === "" ? "../../images/podcast.png" : playerHandler.playicon - icon.width: Theme.iconSizeMedium - icon.height: Theme.iconSizeMedium - icon.color: undefined - onClicked: { - queuehandler.getFirstEntry() + IconButton { + id: playpanelicon + icon.source: playerHandler.playicon + === "" ? "../../images/podcast.png" : playerHandler.playicon + icon.width: Theme.iconSizeMedium + icon.height: Theme.iconSizeMedium + icon.color: undefined + onClicked: { + queuehandler.getFirstEntry() + } + Connections { + target: queuehandler + onFirstEntry: { + pageStack.push(Qt.resolvedUrl( + "../pages/PostDescription.qml"), { + "title": data.title, + "detail": data.detail, + "length": data.length, + "date": data.date, + "duration": data.duration, + "href": data.link + }) + } + } } - Connections { - target: queuehandler - onFirstEntry: { - pageStack.push(Qt.resolvedUrl( - "../pages/PostDescription.qml"), { - "title": data.title, - "detail": data.detail, - "length": data.length, - "date": data.date, - "duration": data.duration, - "href": data.link - }) + IconButton { + id: podimage + icon.source: "image://theme/icon-m-right" + onClicked: pageStack.push(Qt.resolvedUrl("../pages/Player.qml")) + } + Column { + id: statusicons + Icon { + source: "image://theme/icon-s-cloud-download" + visible: playerHandler.streaming + } + Icon { + source: "image://theme/icon-s-timer" + visible: podqast.dosleep + } + Icon { + source: "image://theme/icon-s-duration" + visible: playerHandler.playrate !== 1.0 } } } - IconButton { - id: podimage - icon.source: "image://theme/icon-m-right" - onClicked: pageStack.push(Qt.resolvedUrl("../pages/Player.qml")) - } - Image { - source: "image://theme/icon-s-timer" - visible: podqast.dosleep - } - Image { - source: "image://theme/icon-s-duration" - visible: playerHandler.playrate !== 1.0 + + Item { + id: placeholder + height: parent.height + width: Theme.paddingMedium * 2 + 3 + Rectangle { + height: parent.height - Theme.paddingMedium * 2 + width: 3 + color: Theme.highlightBackgroundColor + visible: additions.visibleChildren.length > 0 + anchors.centerIn: parent + } } - Image { - source: "image://theme/icon-s-cloud-download" - visible: playerHandler.streaming + Row { + id: additions + LayoutMirroring.enabled: true } } } diff --git a/qml/components/PlayerHandler.qml b/qml/components/PlayerHandler.qml index 1689a736013c0c97bc660ff2eebfaed069574334..f97fe200e59fcdb26f9adc95437ef38b42533374 100644 --- a/qml/components/PlayerHandler.qml +++ b/qml/components/PlayerHandler.qml @@ -4,6 +4,9 @@ import io.thp.pyotherside 1.4 Python { id: playerHandler + property var position: mediaplayer.position + property var duration: mediaplayer.duration + property bool isPlaying: false property string playicon: "../../images/podcast.png" property string playtext: "" @@ -12,13 +15,13 @@ Python { property var chapters property int aktchapter property double playpos: 0 - property double playrate: 1.0 + property double playrate: globalPlayrateConf.value property double seekPos property bool doStartAfterSeek: false property bool streaming: mediaplayer.source && !/^file:/.test( mediaplayer.source) - signal playing(string audio_url, int position) + signal playing(string audio_url, int position, bool only_start_if_playing) signal pausing signal stopping signal downloading(int percent) @@ -41,30 +44,23 @@ Python { console.log('python error: ' + traceback) } - onPlayrateChanged: { - seekPos = mediaplayer.position - mediaplayer.playbackRate = playrate - mediaplayer.seek(seekPos) - } - onPlaying: { - console.log("Playing audio_url: " + audio_url) - console.log("Seekable: " + mediaplayer.seekable) + console.info("Playing audio_url: " + audio_url + " @ rate " + playrate) + if (audio_url != "") { - playrate = globalPlayrateConf.value mediaplayer.source = audio_url mediaplayer.seek(position - 15 * 1000) - mediaplayer.play() + if (!only_start_if_playing || mediaplayer.isPlaying) { + mediaplayer.play() + } mediaplayer.seek(position) seekPos = position - } else { + } else if (!only_start_if_playing || mediaplayer.isPlaying) { mediaplayer.play() } - mediaplayer.playbackRate = playrate - - console.log("Duration: ", mediaplayer.duration) - console.log("Position: ", mediaplayer.position) + console.debug("Duration: ", mediaplayer.duration + " Position: ", + mediaplayer.position) isPlaying = true queuehandler.updatePlayingPosition() externalhandler.getAudioData(audio_url) @@ -86,12 +82,12 @@ Python { } function setEpisode(data, chapterlist) { + console.log("setting episode: " + data.title + " url:" + data.audio_url) firstid = data.id firsttitle = data.title chapters = chapterlist playicon = data.logo_url playtext = data.title - mediaplayer.source = data.audio_url } function getaktchapter() { @@ -155,7 +151,7 @@ Python { } function playpause() { - console.debug("Toggle play: " + isPlaying) + console.info("Toggle play: " + isPlaying) if (!isPlaying) { play() } else { @@ -171,6 +167,7 @@ Python { function seek(position) { call("QueueHandler.instance.queue_seek", [position], function () {}) + mediaplayer.seek(position) } function setDuration() { @@ -178,4 +175,22 @@ Python { call("QueueHandler.instance.set_duration", [mediaplayer.duration / 1000], function () {}) } + + function fast_backward() { + var posi = mediaplayer.position + posi = posi - 15 * 1000 + if (posi < 0) { + posi = 0 + } + seek(posi) + } + + function fast_forward() { + var posi = mediaplayer.position + posi = posi + 30 * 1000 + if (posi > mediaplayer.duration) { + posi = mediaplayer.duration + } + seek(posi) + } } diff --git a/qml/components/PodSelectGrid.qml b/qml/components/PodSelectGrid.qml deleted file mode 100644 index 603ceaa310692b08926ea5f999d573b738a029d8..0000000000000000000000000000000000000000 --- a/qml/components/PodSelectGrid.qml +++ /dev/null @@ -1,93 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Nemo.Thumbnailer 1.0 - -SilicaListView { - id: gridview - clip: true - width: parent.width - height: Theme.itemSizeLarge - orientation: ListView.Horizontal - - property string homeicon: "image://theme/icon-m-backup" - property var filter: podqast.hfilter - - // HorizontalScrollDecorator {} - - Connections { - target: archivehandler - onArchivePodList: { - console.log("onArchivePodList") - podselectmodel.clear() - - var keys = Object.keys(data) - - console.log("keys length " + keys.length) - console.log(data) - // gridview.width = keys.length * Theme.itemSizeMedium - gridview.contentWidth = keys.length * Theme.itemSizeMedium - - var thecount = 0 - - for (var i = 0; i < keys.length; i++ ) { - console.log(keys[i]) - console.log(data[keys[i]].count) - console.log(data[keys[i]].logo_url) - thecount += data[keys[i]].count - data[keys[i]].podurl = keys[i] - data[keys[i]].is_enabled = (keys[i] === filter) - - podselectmodel.append(data[keys[i]]) - } - podselectmodel.insert(0, { - podurl: "home", - logo_url : homeicon, - count: thecount, - is_enabled: filter === "home" - }) -// podselectmodel.insert(0, { -// podurl: "none", -// logo_url: "image://theme/icon-m-search", -// count: 0, -// is_enabled: false -// }) - } - } - - model: ListModel { - id: podselectmodel - } - - delegate: ListItem { - width: Theme.iconSizeLarge - height: Theme.iconSizeLarge - highlighted: is_enabled - Image { - asynchronous:true - source: logo_url - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - width: Theme.iconSizeMedium - height: Theme.iconSizeMedium - // opacity: is_enabled ? 1.0 : 0.8 - Label { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.bottom - text: count - font.pixelSize: Theme.fontSizeExtraSmall - font.bold: true - color: Theme.highlightColor - z: 2 - } - MouseArea { - anchors.fill: parent - onClicked: { - console.log(podurl) - if (podurl != "none") { - filter = podurl - } - } - } - } - } -} diff --git a/qml/components/PodqastAudioPlayer.qml b/qml/components/PodqastAudioPlayer.qml new file mode 100644 index 0000000000000000000000000000000000000000..4947647669deaccfa06fd937841c2259d70370c1 --- /dev/null +++ b/qml/components/PodqastAudioPlayer.qml @@ -0,0 +1,56 @@ +import QtQuick 2.0 +import io.thp.pyotherside 1.4 +import QtMultimedia 5.0 + +MediaPlayer { + id: mediaplayer + + readonly property bool isPlaying: playbackState === Audio.PlayingState + + playbackRate: globalPlayrateConf.value + + onSourceChanged: console.log( + "changing mediaplayer src to: " + mediaplayer.source) + + onPlaybackRateChanged: { + if (isPlaying) + seek(position - 0.01) + } + + onPaused: { + console.log("onPaused status: " + mediaplayer.status) + if (mediaplayer.status == 6) { + playerHandler.pause() + } + } + onSeekableChanged: { + console.log("onSeekableChanged status: " + mediaplayer.status + " seekPos: " + + playerHandler.seekPos + " dostartafterSeek:" + playerHandler.doStartAfterSeek) + playerHandler.setDuration() + if (playerHandler.seekPos > 0) { + mediaplayer.seek(playerHandler.seekPos) + playerHandler.seekPos = 0.0 + } + playerHandler.getaktchapter() + if (playerHandler.doStartAfterSeek) { + mediaplayer.play() + playerHandler.doStartAfterSeek = false + } + } + onStatusChanged: { + switch (mediaplayer.status) { + case MediaPlayer.EndOfMedia: + console.log("End of media " + mediaplayer.source) + queuehandler.queueTopToArchive(autoPlayNextInQueue.value) + break + case MediaPlayer.StoppedState: + console.log("Stopped Mediaplayer " + mediaplayer.source) + break + case MediaPlayer.PlayingState: + console.log("Playing " + mediaplayer.source) + break + case MediaPlayer.PausedState: + console.log("Paused " + mediaplayer.source) + } + } +} diff --git a/qml/components/PostListItem.qml b/qml/components/PostListItem.qml index 7fe06f537aecaf44b811f0ef0a4f4b0649e859ad..c3f10b3562c10754c3468f4386887e9495631a25 100644 --- a/qml/components/PostListItem.qml +++ b/qml/components/PostListItem.qml @@ -52,10 +52,12 @@ ListItem { EpisodeImage { id: episodeImage - showDlIcon: episodeImage.percentage > 0 && episodeImage.percentage < 100 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter showIsAudio: isaudio showIsPlaying: postId === playerHandler.firstid showIsListened: listened + percentage: model.dlperc } Column { diff --git a/qml/components/QueueHandler.py b/qml/components/QueueHandler.py index 37bd0f2b6672aef8fdf8522fd83012213ef4005d..4d7a49fde86953cf7debea44e98caa24003c7863 100644 --- a/qml/components/QueueHandler.py +++ b/qml/components/QueueHandler.py @@ -77,7 +77,9 @@ class QueueHandler(object): move element up """ - QueueFactory().get_queue().move_up(id) + if QueueFactory().get_queue().move_up(id) == 0: + await self.send_first_episode_chapters(id) + await self.queue_play(only_start_if_playing=True) await self.get_queue_posts(moved=id) async def queue_move_down(self, id): @@ -88,7 +90,7 @@ class QueueHandler(object): QueueFactory().get_queue().move_down(id) await self.get_queue_posts(moved=id) - async def queue_play(self, ): + async def queue_play(self, only_start_if_playing = False): """ Get the play information from queue """ @@ -100,7 +102,7 @@ class QueueHandler(object): if not data["url"]: pyotherside.send("audioNotExist") else: - pyotherside.send("playing", data["url"], data["position"]) + pyotherside.send("playing", data["url"], data["position"],only_start_if_playing) async def queue_pause(self, position): """ diff --git a/qml/components/QueueHandlerPython.qml b/qml/components/QueueHandlerPython.qml index 002a89fd849608b55b0cfbb828ebb6ba78c0d724..62de9385b671d18f497a2d7f87f89ead46bedf31 100644 --- a/qml/components/QueueHandlerPython.qml +++ b/qml/components/QueueHandlerPython.qml @@ -72,7 +72,7 @@ Python { call("QueueHandler.instance.get_first_entry", function () {}) } function updatePlayingPosition() { - call("QueueHandler.instance.update_position", [mediaplayer.position], + call("QueueHandler.instance.update_position", [playerHandler.position], function () {}) } function downloadAudio(podpost) { diff --git a/qml/components/hints/HintDockIcons.qml b/qml/components/hints/HintDockIcons.qml new file mode 100644 index 0000000000000000000000000000000000000000..e4e3e7c7dcf424ede1fd211413209695235afebf --- /dev/null +++ b/qml/components/hints/HintDockIcons.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Rectangle { + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge + anchors.centerIn: centeredComponent + color: Theme.darkPrimaryColor + Icon { + property string streamIcon: "image://theme/icon-s-cloud-download" + property string sleepTimerIcon: "image://theme/icon-s-timer" + property string playrateIcon: "image://theme/icon-s-duration" + property var sources: [streamIcon, sleepTimerIcon, playrateIcon] + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge + anchors.fill: centeredComponent + property int baseindex: 4 + property int relativeIndex: index - baseindex + source: sources[relativeIndex] + + onRelativeIndexChanged: console.log("relative index" + relativeIndex) + } +} diff --git a/qml/components/hints/HintEpisodeImage.qml b/qml/components/hints/HintEpisodeImage.qml new file mode 100644 index 0000000000000000000000000000000000000000..78a6d88836ddf88a8ba8633fc91b8ac97565c42f --- /dev/null +++ b/qml/components/hints/HintEpisodeImage.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../" + +EpisodeImage { + property int baseindex: 2 + property int relativeIndex: index - baseindex + showDownladingState: false + showIsAudio: relativeIndex === 3 + showIsPlaying: relativeIndex === 1 + showIsListened: relativeIndex === 2 + percentage: relativeIndex === 0 ? 100 : 0 + anchors.fill: centeredComponent + property string logo_url: "../../images/podcast.png" + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge +} diff --git a/qml/components/hints/MainPageUsageHint.qml b/qml/components/hints/MainPageUsageHint.qml new file mode 100644 index 0000000000000000000000000000000000000000..31b3ecf46a69c581a6e75c5ec9508cdfdb31c0b3 --- /dev/null +++ b/qml/components/hints/MainPageUsageHint.qml @@ -0,0 +1,73 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../../lib/hints" +import "../" + +SlideShowHint { + + slides: [{ + "text": qsTr("Hi there, I gonna give you a quick tour to show you basic and new features."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 3 + }, { + "text": qsTr("Quick touch on an episode to see its description, long press it to add it to queue or play it"), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 5 + }, { + "text": qsTr("On an episode image, this icon shows that it is downloaded..."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 3, + "centeredComponent": "../../components/hints/HintEpisodeImage.qml" + }, { + "text": qsTr("...currently playing..."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 2, + "centeredComponent": "../../components/hints/HintEpisodeImage.qml" + }, { + "text": qsTr("...already listened."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 2, + "centeredComponent": "../../components/hints/HintEpisodeImage.qml" + }, { + "text": qsTr("In the dock below, this icon means the file currently played is streaming from the internet right now..."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 4, + "centeredComponent": "../../components/hints/HintDockIcons.qml" + }, { + "text": qsTr("...the sleeptimer is active"), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 2, + "centeredComponent": "../../components/hints/HintDockIcons.qml" + }, { + "text": qsTr("...a playrate different from 1 is set."), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 2, + "centeredComponent": "../../components/hints/HintDockIcons.qml" + }, { + "text": qsTr("You can see whats going on by looking into the log down here"), + "direction": TouchInteraction.Up, + "interactionMode": TouchInteraction.EdgeSwipe, + "loops": 3 + }, { + "text": qsTr("Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings."), + "direction": TouchInteraction.Up, + "interactionMode": TouchInteraction.EdgeSwipe, + "loops": 6 + }] +} diff --git a/qml/components/hints/PlayerHint.qml b/qml/components/hints/PlayerHint.qml new file mode 100644 index 0000000000000000000000000000000000000000..d500d4f2e18681f352f8556b3b09363c23aa612b --- /dev/null +++ b/qml/components/hints/PlayerHint.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../../lib/hints" +import "../" + +SlideShowHint { + slides: [{ + "text": qsTr("Select if the next item of the playlist should be played automatically"), + "direction": TouchInteraction.Down, + "interactionMode": TouchInteraction.EdgeSwipe, + "loops": 3 + }, { + "text": qsTr("Click the image to adjust the playrate"), + "direction": 0, + "interactionMode": 0, + "interactionHidden": true, + "loops": 3 + }] +} diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 9d0e8426046b3c3483ae9819988d9406de134623..3ab6e4bddf9b1fe0a966d05ab07cde7dcb860a63 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -34,7 +34,8 @@ CoverBackground { Label { height: parent.height width: parent.width - text: playerHandler.chapters ? playerHandler.chapters.length + qsTr(" chapters") : "" + text: playerHandler.chapters ? playerHandler.chapters.length + qsTr( + " chapters") : "" font.pixelSize: Theme.fontSizeSmall padding: 10 } @@ -44,20 +45,20 @@ CoverBackground { anchors.top: chaptcount.bottom anchors.horizontalCenter: parent.horizontalCenter Label { - text: to_pos_str(mediaplayer.position / 1000) + text: to_pos_str(playerHandler.position / 1000) font.bold: true font.pixelSize: Theme.fontSizeLarge color: Theme.highlightColor anchors.horizontalCenter: parent.horizontalCenter } Label { - text: to_pos_str((mediaplayer.duration - mediaplayer.position) / 1000) + text: to_pos_str( + (playerHandler.duration - playerHandler.position) / 1000) font.bold: true font.pixelSize: Theme.fontSizeLarge color: Theme.highlightColor anchors.horizontalCenter: parent.horizontalCenter } - } CoverActionList { diff --git a/qml/harbour-podqast.qml b/qml/harbour-podqast.qml index 07b766d5dc6b1f759b8cc122409c5df98f7d06ff..4cdb4e8cc624eb9e1fb359ead7a8d39561067965 100644 --- a/qml/harbour-podqast.qml +++ b/qml/harbour-podqast.qml @@ -397,49 +397,6 @@ ApplicationWindow { } } - Connections { - target: mediaplayer - - ignoreUnknownSignals: true - onPaused: { - console.log("onPaused status: " + mediaplayer.status) - if (mediaplayer.status == 6) { - playerHandler.pause() - } - } - onSeekableChanged: { - console.log("onSeekableChanged status: " + mediaplayer.status - + " seekPos: " + playerHandler.seekPos - + " dostartafterSeek:" + playerHandler.doStartAfterSeek) - playerHandler.setDuration() - if (playerHandler.seekPos > 0) { - mediaplayer.seek(playerHandler.seekPos) - playerHandler.seekPos = 0.0 - } - playerHandler.getaktchapter() - if (playerHandler.doStartAfterSeek) { - mediaplayer.play() - playerHandler.doStartAfterSeek = false - } - } - onStatusChanged: { - switch (mediaplayer.status) { - case MediaPlayer.EndOfMedia: - console.log("End of media " + mediaplayer.source) - queuehandler.queueTopToArchive(autoPlayNextInQueue.value) - break - case MediaPlayer.StoppedState: - console.log("Stopped Mediaplayer " + mediaplayer.source) - break - case MediaPlayer.PlayingState: - console.log("Playing " + mediaplayer.source) - break - case MediaPlayer.PausedState: - console.log("Paused " + mediaplayer.source) - } - } - } - Connections { target: externalhandler onAudioInfo: { @@ -479,6 +436,10 @@ ApplicationWindow { id: loghandler } + PodqastAudioPlayer { + id: mediaplayer + } + PlayerHandler { id: playerHandler onAudioNotExist: { @@ -495,38 +456,9 @@ ApplicationWindow { id: favoritehandler } - Item { - MediaPlayer { - id: mediaplayer - - onSourceChanged: console.log( - "changing mediaplayer src to: " + mediaplayer.source) - } - } - function to_pos_str(secs) { var date = new Date(null) date.setSeconds(secs) return date.toISOString().substr(11, 8) } - - function fast_backward() { - var posi = mediaplayer.position - posi = posi - 15 * 1000 - if (posi < 0) { - posi = 0 - } - playerHandler.seek(posi) - mediaplayer.seek(posi) - } - - function fast_forward() { - var posi = mediaplayer.position - posi = posi + 30 * 1000 - if (posi > mediaplayer.duration) { - posi = mediaplayer.duration - } - playerHandler.seek(posi) - mediaplayer.seek(posi) - } } diff --git a/qml/lib/hints/SingleTimeHint.qml b/qml/lib/hints/SingleTimeHint.qml new file mode 100644 index 0000000000000000000000000000000000000000..80cad73c055be161a08f5d49e0c5dea15987133c --- /dev/null +++ b/qml/lib/hints/SingleTimeHint.qml @@ -0,0 +1,20 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../../components/hints" + +FirstTimeUseCounter { + id: firstTimeUseCounter + limit: 2 + defaultValue: 1 + property string hintComponent + onActiveChanged: { + if (value < limit) { + console.info("Showing hint " + hintComponent) + var comp = Qt.createComponent(hintComponent) + if (comp.status === Component.Ready) { + var obj = comp.createObject(parent) + } + firstTimeUseCounter.increase() + } + } +} diff --git a/qml/lib/hints/SlideShowHint.qml b/qml/lib/hints/SlideShowHint.qml new file mode 100644 index 0000000000000000000000000000000000000000..c121957c0c8d586c838c87acce7bfc47abe6e381 --- /dev/null +++ b/qml/lib/hints/SlideShowHint.qml @@ -0,0 +1,85 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + anchors.fill: slideNr < slides.length ? parent : null + height: slideNr < slides.length ? parent.height : 0 + + property var slides: [] + property int slideNr: -1 + property var slideconfig: null + property bool slideloaded: true + + MouseArea { + anchors.fill: parent + + onClicked: { + console.info("clicked hint") + touchInteractionHint.stop() + } + } + + Component.onCompleted: nextSlide() + + Loader { + id: centeredComponent + anchors.centerIn: parent + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge + property int index: slideNr + source: !slideloaded ? "" : slideconfig.centeredComponent + visible: !slideloaded ? false : slideconfig.centeredComponent !== undefined + active: !slideloaded ? false : slideconfig.centeredComponent !== undefined + onActiveChanged: console.info("loading component " + source) + } + + InteractionHintLabel { + anchors.bottom: parent.bottom + width: parent.width + visible: true + + opacity: touchInteractionHint.running ? 1.0 : 0.0 + Behavior on opacity { + FadeAnimation { + duration: 750 + } + } + + text: !slideloaded ? "" : slideconfig.text + } + Item { + anchors.fill: parent + visible: slideloaded && slideconfig.interactionHidden !== true + onVisibleChanged: console.info( + "Slide " + slideNr + " hides the touchInteractionHint: " + !visible) + TouchInteractionHint { + id: touchInteractionHint + + direction: !slideloaded ? -1 : slideconfig.direction + interactionMode: !slideloaded ? -1 : slideconfig.interactionMode + anchors.verticalCenter: parent.bottom + loops: !slideloaded ? 0 : slideconfig.loops + onRunningChanged: if (!running && slideNr < slides.length) { + console.info("running changed switched to next slide") + nextSlide() + } + } + } + + function nextSlide() { + slideNr++ + console.info("nextslide: " + slideNr) + if (slideNr < slides.length) { + slideconfig = slides[slideNr] + slideloaded = true + console.info("switching to hint slide " + (slideNr) + " : " + JSON.stringify( + slideconfig)) + touchInteractionHint.restart() + } else { + slideloaded = false + slideconfig = null + centeredComponent.visible = false + touchInteractionHint.stop() + } + } +} diff --git a/qml/pages/Archive.qml b/qml/pages/Archive.qml index 71ea99a2b09f603a2cda63171655e98faa38efc3..bc7dc9fa8e2fd44dc5ce38b5af5e437f4313e7f2 100644 --- a/qml/pages/Archive.qml +++ b/qml/pages/Archive.qml @@ -1,6 +1,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +import "../lib/hints" import "../components" +import "../components/hints" Page { id: page @@ -11,10 +13,14 @@ Page { SilicaFlickable { anchors.fill: parent - VerticalScrollDecorator { } + VerticalScrollDecorator {} - AppMenu { thispage: "Archive" } - PrefAboutMenu { thelink: "https://gitlab.com/cy8aer/podqast/wikis/help-library" } + AppMenu { + thispage: "Archive" + } + PrefAboutMenu { + thelink: "https://gitlab.com/cy8aer/podqast/wikis/help-library" + } // Tell SilicaFlickable the height of its content. contentHeight: page.height @@ -37,11 +43,12 @@ Page { anchors.verticalCenter: parent.verticalCenter IconButton { id: refreshicon - enabled: refreshcol.refreshprogress == 1.0 && ! archivetitle.refreshing + enabled: refreshcol.refreshprogress == 1.0 + && !archivetitle.refreshing icon.source: "image://theme/icon-m-refresh" onClicked: { // refresh() - if (wifiConnected || doMobileDownConf.value ) { + if (wifiConnected || doMobileDownConf.value) { archivetitle.refreshing = true feedparserhandler.refreshPodcasts() } @@ -98,31 +105,35 @@ Page { console.log("Data length: " + pcdata.length) podcastsModel.clear() podcastsModel.append({ - bethead: "00title", - url: "", - description: "", - logo_url: "image://theme/icon-m-backup", - title: qsTr("History"), - topage: "History.qml"}) + "bethead": "00title", + "url": "", + "description": "", + "logo_url": "image://theme/icon-m-backup", + "title": qsTr("History"), + "topage": "History.qml" + }) podcastsModel.append({ - bethead: "00title", - url: "", - description: "", - logo_url: "image://theme/icon-m-favorite-selected", - title: qsTr("Favorites"), - topage: "Favorites.qml"}) - if(allowExtConf.value) { + "bethead": "00title", + "url": "", + "description": "", + "logo_url": "image://theme/icon-m-favorite-selected", + "title": qsTr("Favorites"), + "topage": "Favorites.qml" + }) + if (allowExtConf.value) { podcastsModel.append({ - bethead: "0ext", - url: "", - description: "", - logo_url: "image://theme/icon-m-device-upload", - title: qsTr("External Audio"), - topage: "External.qml"}) + "bethead": "0ext", + "url": "", + "description": "", + "logo_url": "image://theme/icon-m-device-upload", + "title": qsTr( + "External Audio"), + "topage": "External.qml" + }) } for (var i = 0; i < pcdata.length; i++) { pcdata[i]["bethead"] = "Podcast" - podcastsModel.append(pcdata[i]); + podcastsModel.append(pcdata[i]) } } } @@ -131,15 +142,22 @@ Page { enabled: podcastsModel.count == 0 text: qsTr("Rendering") hintText: qsTr("Collecting Podcasts") - verticalOffset: - archivetitle.height + verticalOffset: -archivetitle.height } model: ListModel { id: podcastsModel } - delegate: PodcastItem { } + delegate: PodcastItem {} } - PlayDockedPanel { id: pdp } + PlayDockedPanel { + id: pdp + } + } + + SingleTimeHint { + key: "/apps/ControlPanel/podqast/hints/archive_hint" + hintComponent: "../../components/hints/MainPageUsageHint.qml" } } diff --git a/qml/pages/Chapters.qml b/qml/pages/Chapters.qml index f737db19620f73c9752bcbc3da638172897e9881..0390e9b183c637332c0686d3ab593c93daa715bd 100644 --- a/qml/pages/Chapters.qml +++ b/qml/pages/Chapters.qml @@ -9,12 +9,11 @@ Page { property var chapterid property bool currentlyPlayingEpisode: (episodeid === playerHandler.firstid - && playerHandler.isPlaying === true) + && playerHandler.isPlaying === true) // The effective value will be restricted by ApplicationWindow.allowedOrientations allowedOrientations: Orientation.All - Component.onCompleted: { chapterModel.clear() queuehandler.getEpisodeChapters(episodeid) @@ -25,7 +24,8 @@ Page { onEpisodeChapters: { for (var i = 0; i < chapters.length; i++) { - console.log("got chapter",i,chapters[i].selected,chapters[i].title) + console.log("got chapter", i, chapters[i].selected, + chapters[i].title) chapterModel.append(chapters[i]) } } @@ -46,7 +46,7 @@ Page { height: page.height header: PageHeader { - title: qsTr("Chapters") + title: qsTr("Chapters") } model: ListModel { @@ -65,10 +65,12 @@ Page { text: title description: msToTime(start_millis) checked: selected - busy: (currentlyPlayingEpisode && playerHandler.aktchapter === index) + busy: (currentlyPlayingEpisode + && playerHandler.aktchapter === index) onClicked: { - queuehandler.toggleChapter(episodeid, chapterid=index) - console.log(episodeid,playerHandler.firstid,playerHandler.isPlaying) + queuehandler.toggleChapter(episodeid, chapterid = index) + console.log(episodeid, playerHandler.firstid, + playerHandler.isPlaying) if (currentlyPlayingEpisode) { console.log("changed chapter selection of currently playing episode") queuehandler.sendFirstEpisodeChapters(episodeid) @@ -77,17 +79,17 @@ Page { function msToTime(s) { function pad(n, z) { - z = z || 2; - return ('00' + n).slice(-z); - } - var ms = s % 1000; - s = (s - ms) / 1000; - var secs = s % 60; - s = (s - secs) / 60; - var mins = s % 60; - var hrs = (s - mins) / 60; - - return pad(hrs) + ':' + pad(mins) + ':' + pad(secs); + z = z || 2 + return ('00' + n).slice(-z) + } + var ms = s % 1000 + s = (s - ms) / 1000 + var secs = s % 60 + s = (s - secs) / 60 + var mins = s % 60 + var hrs = (s - mins) / 60 + + return pad(hrs) + ':' + pad(mins) + ':' + pad(secs) } } @@ -98,9 +100,7 @@ Page { icon.source: "image://theme/icon-m-next" onClicked: { var millis = start_millis - console.log(millis) playerHandler.seek(millis) - mediaplayer.seek(millis) } } } diff --git a/qml/pages/DataMigration.qml b/qml/pages/DataMigration.qml index cc2e14e960f0f22ce7982f23ae56757166df433a..430a053d180c5b7ffc83f03d3d1d57b8967037fe 100644 --- a/qml/pages/DataMigration.qml +++ b/qml/pages/DataMigration.qml @@ -76,7 +76,7 @@ Page { width: parent.width wrapMode: Text.WordWrap //: whatsnew section of the migration - text: qsTr("Whats New?If you want to contribute help translating the app or report issues on gitlab.") + text: qsTr("Whats New?
If you want to contribute to podQast, you can help translating the app or report issues on GitLab.") } Label { diff --git a/qml/pages/History.qml b/qml/pages/History.qml index 16c1f7d81c2e99f4849eae500b4730d909a73300..f62ca109563e56deb63e7ffefeab25a64bd2cd06 100644 --- a/qml/pages/History.qml +++ b/qml/pages/History.qml @@ -50,7 +50,6 @@ Page { SilicaListView { id: archivepostlist clip: true - // anchors.top: podselectgrid.bottom anchors.top: archivetitle.bottom width: parent.width height: page.height - pdp.height - archivetitle.height diff --git a/qml/pages/Inbox.qml b/qml/pages/Inbox.qml index daf11e588da97df6893bf908cd9b79c3505d53cd..2d929f4d894e2ebb1578de790bc7954edd9e7947 100644 --- a/qml/pages/Inbox.qml +++ b/qml/pages/Inbox.qml @@ -34,7 +34,6 @@ Page { } onGetInboxPosts: { inboxhandler.getInboxEntries(podqast.ifilter) - inboxhandler.getInboxPodData() } } @@ -42,7 +41,6 @@ Page { target: feedparserhandler onRefreshFinished: { inboxhandler.getInboxEntries(podqast.ifilter) - inboxhandler.getInboxPodData() } } @@ -77,30 +75,12 @@ Page { } } - // PodSelectGrid { - // id: podselectgrid - // homeicon: "image://theme/icon-m-media-playlists" - // anchors.top: inboxtitle.bottom - // height: Theme.iconSizeLarge - // z: 3 - // filter: podqast.ifilter - // Component.onCompleted: { - // // inboxhandler.getInboxPodData() - // } - // onFilterChanged: { - // podqast.ifilter = filter - // inboxPostModel.clear() - // inboxhandler.getInboxPodData() - // inboxhandler.getInboxEntries(podqast.ifilter) - // } - // } SilicaListView { id: inboxpostlist clip: true - // anchors.top: podselectgrid.bottom anchors.top: inboxtitle.bottom width: parent.width - height: page.height - pdp.height - inboxtitle.height // - podselectgrid.height + height: page.height - pdp.height - inboxtitle.height section.property: 'section' section.delegate: SectionHeader { text: section @@ -110,7 +90,7 @@ Page { enabled: inboxPostModel.count == 0 text: qsTr("No new posts") hintText: qsTr("Pull down to Discover new podcasts, get posts from Library, or play the Playlist") - verticalOffset: -inboxtitle.height // - podselectgrid.height + verticalOffset: -inboxtitle.height } model: ListModel { diff --git a/qml/pages/Log.qml b/qml/pages/Log.qml index b2793a7d37fb203d476134f59376aae117dd2266..b6c8ea618979c4d74cb5ff42f1f0ecb7cfb489aa 100644 --- a/qml/pages/Log.qml +++ b/qml/pages/Log.qml @@ -33,9 +33,8 @@ Page { delegate: Column { clip: true - width: ListView.view.width - anchors.leftMargin: Theme.paddingMedium - anchors.rightMargin: Theme.paddingMedium + x: Theme.horizontalPageMargin + width: parent.width - 2 * x Label { wrapMode: Text.WordWrap width: parent.width diff --git a/qml/pages/Player.qml b/qml/pages/Player.qml index 03379ce8193f8efed92001d5586127dd95faf78e..e74c28728f93de66e64ba1cd41c7bd4d8786adb1 100644 --- a/qml/pages/Player.qml +++ b/qml/pages/Player.qml @@ -1,5 +1,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +import "../lib/hints" +import "../components/hints" +import "../components" Page { id: page @@ -30,21 +33,37 @@ Page { opacity: 0.8 } - Slider { - id: playrateSlider - width: parent.width - label: qsTr("Audio playrate") - minimumValue: 0.85 - maximumValue: 2.0 - handleVisible: true - valueText: "1:" + value - stepSize: 0.15 - value: playerHandler.playrate - onValueChanged: playerHandler.playrate = value + Item { + id: playrateRow + width: parent.width - 2 * Theme.horizontalPageMargin + height: Theme.itemSizeExtraLarge + Icon { + id: playrateIcon + source: "image://theme/icon-s-duration" + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + anchors.verticalCenter: parent.verticalCenter + } + + Slider { + id: playrateSlider + anchors.left: playrateIcon.right + anchors.right: parent.right + label: qsTr("Audio playrate") + minimumValue: 0.75 + maximumValue: 2.5 + handleVisible: true + valueText: "1:" + value + stepSize: 0.05 + value: playerHandler.playrate + onValueChanged: { + globalPlayrateConf.value = value + } + } } - Row { - anchors.top: playrateSlider.bottom - width: parent.width + Item { + anchors.top: playrateRow.bottom + width: parent.width - 2 * Theme.horizontalPageMargin height: Theme.itemSizeExtraLarge IconButton { id: sleepButton @@ -52,7 +71,6 @@ Page { icon.source: "image://theme/icon-m-timer" highlighted: podqast.dosleep onClicked: { - // highlighted = !highlighted podqast.dosleep = !podqast.dosleep } @@ -143,7 +161,8 @@ Page { height: Theme.itemSizeExtraLarge IconButton { id: lastchapter - enabled: playerHandler.chapters.length > 0 + enabled: playerHandler.chapters !== undefined + && playerHandler.chapters.length > 0 anchors.left: parent.left icon.source: "image://theme/icon-m-previous" onClicked: { @@ -152,7 +171,6 @@ Page { playerHandler.aktchapter -= 1 playerHandler.playpos = playerHandler.chapters[playerHandler.aktchapter].start_millis playerHandler.seek(playerHandler.playpos) - mediaplayer.seek(playerHandler.playpos) chapterLabel.text = Number( playerHandler.aktchapter + 1) + ". " + playerHandler.chapters[playerHandler.aktchapter].title @@ -239,11 +257,11 @@ Page { rightMargin: 50 height: parent.height handleVisible: false - maximumValue: mediaplayer.duration - value: mediaplayer.position - valueText: to_pos_str(mediaplayer.position / 1000) + maximumValue: playerHandler.duration + value: playerHandler.position + valueText: to_pos_str(playerHandler.position / 1000) onPressedChanged: { - mediaplayer.seek(playSlider.value) + playerHandler.seek(playSlider.value) } } } @@ -255,7 +273,7 @@ Page { IconButton { icon.source: "image://theme/icon-m-previous" onClicked: { - podqast.fast_backward() + playerHandler.fast_backward() } } IconButton { @@ -269,11 +287,16 @@ Page { icon.source: "image://theme/icon-m-next" onClicked: { - podqast.fast_forward() + playerHandler.fast_forward() } } } } } } + + SingleTimeHint { + key: "/apps/ControlPanel/podqast/hints/player_hint" + hintComponent: "../../components/hints/PlayerHint.qml" + } } diff --git a/qml/pages/PodcastSettings.qml b/qml/pages/PodcastSettings.qml index 8c2a0476c45f0514c47382adae739171c2d17895..adba58b795ab873a238a7d313cb65f9164a4f769 100644 --- a/qml/pages/PodcastSettings.qml +++ b/qml/pages/PodcastSettings.qml @@ -4,10 +4,22 @@ import Sailfish.Silica 1.0 Dialog { property var url property var podtitle + property var feedinfo + + DialogHeader { + id: header + title: qsTr("Settings") + } SilicaFlickable { - anchors.fill: parent - contentWidth: parent.width + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + margins: Theme.paddingMedium + } + contentWidth: parent.width - 2 * Theme.paddingMedium VerticalScrollDecorator {} @@ -15,13 +27,54 @@ Dialog { id: downloadConf width: parent.width - DialogHeader { - title: podtitle + Label { + text: podtitle + font.pixelSize: Theme.fontSizeLarge + color: Theme.highlightColor + wrapMode: Text.WordWrap + width: parent.width + } + + Label { + text: qsTr("%1 Posts").arg(feedinfo["episodecount"]) + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + } + + Label { + text: qsTr("URL") + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + } + + LinkedLabel { + plainText: url + wrapMode: Text.WrapAnywhere + width: parent.width } + Label { - text: qsTr("Posts") + text: qsTr("Link") font.pixelSize: Theme.fontSizeMedium color: Theme.highlightColor + visible: linklabel.visible + } + + LinkedLabel { + id: linklabel + plainText: feedinfo["link"] + wrapMode: Text.WrapAnywhere + visible: plainText.length > 0 + width: parent.width + } + Item { + height: Theme.itemSizeLarge + width: parent.width + Separator { + anchors.centerIn: parent + color: Theme.highlightColor + width: parent.width + } } ComboBox { @@ -75,6 +128,7 @@ Dialog { onOpened: { console.log("Title: " + podtitle) feedparserhandler.getPodcastParams(url) + feedparserhandler.getPodcast(url) } onAccepted: { @@ -89,7 +143,6 @@ Dialog { Connections { target: feedparserhandler onPodcastParams: { - console.log("podcast moveto: " + pcdata.move) if (pcdata.move !== -1) { moveTo.currentIndex = pcdata.move } else { @@ -106,8 +159,9 @@ Dialog { } else { playrate.value = globalPlayrateConf.value } - - console.log("moveTo:" + moveTo.currentIndex) + } + onFeedInfo: { + feedinfo = pcdata } } } diff --git a/qml/pages/PodpostList.qml b/qml/pages/PodpostList.qml index a3c469e45c883adddaf9e94bcb423b0351d2f996..39e815e0ec7a26b25b7004a0e39eae4afe187bfd 100644 --- a/qml/pages/PodpostList.qml +++ b/qml/pages/PodpostList.qml @@ -13,16 +13,42 @@ Page { allowedOrientations: Orientation.All Component.onCompleted: { - feedparserhandler.getEntries(url) + get_data() + feedparserhandler.getPodcast(url) + } + + property string searchtext: "" + property string order: "published" + property string order_dir: "desc" + property bool fav_filter: false + // 2 do not filter, 0 not listened, 1 listened + property int listened_filter: 2 + + function get_data() { + var get_options = { + "podcast_url": url, + "searchtext": searchtext, + "fav_filter": fav_filter, + "order": order, + "order_dir": order_dir + } + if (listened_filter < 2) + get_options["listened_filter"] = listened_filter === 1 + feedparserhandler.getEntries(get_options) } Connections { target: feedparserhandler ignoreUnknownSignals: true + + onFeedinfo: { + title = pcdata["title"] + link = pcdata["link"] + } + onEpisodeListData: { - title = podtitle - link = podlink if (offset == 0) { + console.log("reloading data") podpostsModel.clear() } for (var i = 0; i < episodes.length; i++) { @@ -46,24 +72,126 @@ Page { // Tell SilicaFlickable the height of its content. contentHeight: page.height - Column { - id: archivetitle + Column { + id: archivetitle + + width: page.width + + spacing: Theme.paddingLarge + PageHeader { + title: page.title + Column { + id: placeholder + width: Theme.itemSizeExtraSmall / 2 + } + + Column { + id: prefscol + anchors.verticalCenter: parent.verticalCenter + anchors.left: placeholder.right + } + } + } + + SilicaListView { + id: archivepostlist + clip: true + width: parent.width + height: page.height - pdp.height - archivetitle.height - searchControls.height + section.property: 'section' + section.delegate: SectionHeader { + text: section + horizontalAlignment: Text.AlignRight + } - width: page.width + ViewPlaceholder { + enabled: podpostsModel.count == 0 + text: qsTr("Rendering") + hintText: qsTr("Creating items") + verticalOffset: -archivetitle.height + } + + model: ListModel { + id: podpostsModel + } + delegate: PodpostPostListItem {} + } - spacing: Theme.paddingLarge - PageHeader { - title: page.title - Column { - id: placeholder - width: Theme.itemSizeExtraSmall / 2 + Column { + id: searchControls + width: parent.width + visible: false + + SearchField { + id: episodeSearchField + width: parent.width + height: visible ? Theme.itemSizeMedium : 0 + visible: false + onTextChanged: { + searchtext = episodeSearchField.text + get_data() + } } - Column { - id: prefscol - anchors.verticalCenter: parent.verticalCenter - anchors.left: placeholder.right + Row { + id: episodeSortField + width: parent.width + visible: false + height: visible ? Theme.itemSizeMedium : 0 + LayoutMirroring.enabled: true + + IconButton { + icon.source: fav_filter ? "image://theme/icon-m-favorite-selected" : "image://theme/icon-m-favorite" + icon.color: fav_filter ? Theme.highlightColor : Theme.primaryColor + onClicked: { + fav_filter = !fav_filter + get_data() + } + } + + IconButton { + icon.source: "image://theme/icon-m-headphone" + icon.color: listened_filter < 2 ? Theme.highlightColor : Theme.primaryColor + onClicked: { + listened_filter++ + listened_filter = listened_filter % 3 + get_data() + } + Icon { + source: "image://theme/icon-s-clear-opaque-cross" + visible: listened_filter === 0 + color: Theme.highlightColor + } + } + + IconButton { + icon.source: order_dir == "asc" ? "image://theme/icon-m-page-up" : "image://theme/icon-m-page-down" + icon.color: Theme.highlightColor + onClicked: { + if (order_dir == "desc") + order_dir = "asc" + else + order_dir = "desc" + get_data() + } + } + } + + Row { + id: settingsrow + height: visible ? Theme.itemSizeMedium : 0 + width: parent.width + visible: parent.visible + LayoutMirroring.enabled: true + + IconButton { + icon.source: "image://theme/icon-m-cancel" + onClicked: { + searchControls.visible = false + } + } + IconButton { id: prefsicon icon.source: "image://theme/icon-m-developer-mode" @@ -75,37 +203,32 @@ Page { }) } } - } - } - } - SilicaListView { - id: archivepostlist - clip: true - anchors.top: archivetitle.bottom - width: parent.width - height: page.height - pdp.height - archivetitle.height - section.property: 'section' - section.delegate: SectionHeader { - text: section - horizontalAlignment: Text.AlignRight - } - - ViewPlaceholder { - enabled: podpostsModel.count == 0 - text: qsTr("Rendering") - hintText: qsTr("Creating items") - verticalOffset: -archivetitle.height - } + IconButton { + id: searchicon + icon.source: "image://theme/icon-m-search" + onClicked: { + episodeSearchField.visible = !episodeSearchField.visible + } + } - model: ListModel { - id: podpostsModel + IconButton { + id: sorticon + icon.source: "image://theme/icon-m-transfer" + onClicked: { + episodeSortField.visible = !episodeSortField.visible + } + } + } } - delegate: PodpostPostListItem {} } - PlayDockedPanel { id: pdp + + IconButton { + icon.source: "image://theme/icon-m-search" + onClicked: searchControls.visible = !searchControls.visible + } } } } diff --git a/qml/pages/Settings.qml b/qml/pages/Settings.qml index 4200900b1ff3dc4aaeb85ecb08489090d29b1023..555b361860c8650688f6855ffb4df0de0ed1d65f 100644 --- a/qml/pages/Settings.qml +++ b/qml/pages/Settings.qml @@ -148,6 +148,15 @@ Dialog { valueText: value + qsTr("min") stepSize: 1 } + TextSwitch { + anchors { + left: parent.left + right: parent.right + margins: Theme.paddingMedium + } + id: autoPlayQueue + text: qsTr("Auto-play next episode in queue") + } Label { anchors { @@ -353,12 +362,14 @@ Dialog { experimentalFlag.checked = experimentalConf.value sleepTimer.value = sleepTimerConf.value listenedThreshold.value = markListenedEndThresholdConf.value + autoPlayQueue.checked = autoPlayNextInQueue.value // downLimit.value = downLimitConf.value // refreshTime = refreshTimeConf.value } onAccepted: { // useGpodderConf.value = useGpodder.checked dataTrackableConf.value = dataTrackable.checked + autoPlayNextInQueue.value = autoPlayQueue.checked feedparserhandler.nomedia(dataTrackableConf.value) diff --git a/test/conftest.py b/test/conftest.py index 26f81ae60fb5cf907c4428126b091426d56e9ce5..6d7637324eec47acf7a5652d54a74461d7207440 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,14 +2,22 @@ import logging import pytest + + @pytest.fixture(autouse=True, scope="function") def cleanup(): cleanup_archive() cleanup_queue() cleanup_inbox() cleanup_podcast() + cleanup_log() yield +def cleanup_log(): + from podcast.persistent_log import LogMessage + LogMessage.delete().execute() + + def cleanup_inbox(): from podcast.inbox import InboxEntry InboxEntry.delete().execute() diff --git a/test/test_favorites.py b/test/test_favorites.py index b8cb9b82977f2a473962e59d3af52c0fc3fbf26b..c20e39943f6112a6a0419f9c7989391895dd62f7 100644 --- a/test/test_favorites.py +++ b/test/test_favorites.py @@ -27,7 +27,4 @@ def test_queue_favorites(): post.favorite = True PodpostFactory().persist(post) archive.insert(post.id) - assert len(favorite.get_queue_favorites()) == 0 assert ilen(favorite.get_favorites()) == 5 - assert len(favorite.get_favorite_podcast_stats()) == 1 - assert favorite.get_favorite_podcast_stats()[url]['count'] == 5 diff --git a/test/test_feedutils.py b/test/test_feedutils.py new file mode 100644 index 0000000000000000000000000000000000000000..ac4293d77985f5fb64454132cc71c94cb2098625 --- /dev/null +++ b/test/test_feedutils.py @@ -0,0 +1,16 @@ +import httpretty + +from podcast.feedutils import fetch_feed, NewFeedUrlError +from test import read_testdata, xml_headers + + +@httpretty.activate +def test_new_feed_url(): + url_f = "https://fakefeed.com/" + httpretty.HTTPretty.register_uri(httpretty.HTTPretty.GET, url_f, body=read_testdata('testdata/new-feed-url-feed.rss'), + adding_headers=xml_headers) + try: + fetch_feed(None,url_f) + assert False + except NewFeedUrlError: + pass diff --git a/test/test_podcast.py b/test/test_podcast.py index 78b843dd511211ee4f9e318c5c4a6829f9883c6e..3eda9cb654bbd517a6fce549e9958545bedd985f 100644 --- a/test/test_podcast.py +++ b/test/test_podcast.py @@ -7,9 +7,11 @@ from typing import Tuple, List import httpretty import pytest from httpretty import HTTPretty +from more_itertools import first from peewee import DoesNotExist from podcast import POST_ID_TYPE +from podcast.persistent_log import get_log_messages, LogType from podcast.podcastlist import PodcastList from podcast.util import ilen from test import xml_headers, read_testdata @@ -27,6 +29,9 @@ def test_feed_entry(): Get a feed entry """ url_sn = "http://feeds.twit.tv/sn.xml" + HTTPretty.register_uri(HTTPretty.GET, url_sn, + body=read_testdata('testdata/twittv.xml'), adding_headers=xml_headers) + url_sn = "https://feeds.twit.tv/sn.xml" HTTPretty.register_uri(HTTPretty.GET, url_sn, body=read_testdata('testdata/twittv.xml'), adding_headers=xml_headers) url_fs = 'https://freakshow.fm/feed/opus/' @@ -42,7 +47,6 @@ def test_feed_entry(): '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) - seen_postids = {} pclist = PodcastList() @@ -185,8 +189,9 @@ def test_feed_no_changes(): assert invoked == 2 -def test_feed_refresh(refreshable_podcast): - p, episodes = refreshable_podcast +@pytest.mark.parametrize("refreshable_podcast_fixture", ["testdata/fakefeed"], indirect=True) +def test_feed_refresh(refreshable_podcast_fixture): + p, episodes = refreshable_podcast_fixture assert len(episodes) == 1 new_episodes = list(p.refresh(0)) assert len(new_episodes) == 1 @@ -195,24 +200,30 @@ def test_feed_refresh(refreshable_podcast): 'Hello, I am a fake episode for testing!!öäü'] -def test_podcastlist_refresh(refreshable_podcast): - p, episodes = refreshable_podcast +@pytest.mark.parametrize("refreshable_podcast_fixture", ["testdata/fakefeed"], indirect=True) +def test_podcastlist_refresh(refreshable_podcast_fixture): + p, episodes = refreshable_podcast_fixture plist = PodcastList() assert ilen(p.get_entries()) == 1 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.fixture() -def refreshable_podcast() -> Tuple[Podcast, List[Podpost]]: +@pytest.fixture +def refreshable_podcast_fixture(request) -> Tuple[Podcast, List[Podpost]]: + """ + A pytest fixture to setup a feed that uses a different file when refreshed. + @param request: the first parameter should be the filename without xml. Thus loads .xml and then 2.xml + """ httpretty.enable() invoked = 0 + filename = request.param def request_callback(request, uri, response_headers): nonlocal invoked invoked += 1 logger.info("Returning normal response file") - testdata = read_testdata('testdata/fakefeed.xml') if invoked == 1 else read_testdata('testdata/fakefeed2.xml') + testdata = read_testdata(filename + ".xml") if invoked == 1 else read_testdata(filename + "2.xml") return [200, response_headers, testdata] feed_url = 'http://fakefeed.com/feed' @@ -221,6 +232,7 @@ def refreshable_podcast() -> Tuple[Podcast, List[Podpost]]: yield Podcast.create_from_url(feed_url) assert invoked > 0 + @httpretty.activate def test_pagination(): url_f = "https://fakefeed.com/page" @@ -230,20 +242,22 @@ def test_pagination(): assert 3 == podcast.count_episodes() assert 3 == len(episodes) + @httpretty.activate def test_pagination_stops(): url_f1 = "https://fakefeed.com/" url_f2 = "https://fakefeed.com/page2" invoked = 0 + def request_callback(request, uri, response_headers): nonlocal invoked invoked += 1 logger.info("Returning normal response file") - testdata = read_testdata('testdata/fakefeed.xml') if invoked == 1 else read_testdata('testdata/pagedfakefeed.xml') + testdata = read_testdata('testdata/fakefeed.xml') if invoked == 1 else read_testdata( + 'testdata/pagedfakefeed.xml') return [200, response_headers, testdata] - HTTPretty.register_uri(HTTPretty.GET, url_f1, body=request_callback, adding_headers=xml_headers) HTTPretty.register_uri(HTTPretty.GET, url_f2, body=read_testdata('testdata/fakefeed.xml'), @@ -256,6 +270,7 @@ def test_pagination_stops(): assert 2 == podcast.count_episodes() assert 1 == len(episodes) + @httpretty.activate def test_pagination(): url_f = "https://fakefeed.com/page" @@ -265,5 +280,29 @@ def test_pagination(): assert 2 == podcast.count_episodes() Podpost.delete_by_id(episodes[1].id) assert 1 == podcast.count_episodes() - list(podcast.refresh(0,0,True)) + list(podcast.refresh(0, 0, True)) assert 2 == podcast.count_episodes() + + +@httpretty.activate +def test_new_feed_url(): + url_f = "https://fakefeed.com/" + HTTPretty.register_uri(HTTPretty.GET, url_f, body=read_testdata('testdata/new-feed-url-feed.rss'), + adding_headers=xml_headers) + url_f1 = "https://newhoster.com/feed/ogg" + HTTPretty.register_uri(HTTPretty.GET, url_f1, body=read_testdata('testdata/fakefeed.xml'), + adding_headers=xml_headers) + podcast, episodes = Podcast.create_from_url(url_f) + assert podcast.url == "https://newhoster.com/feed/ogg" + assert 1 == ilen(get_log_messages()) + assert list(get_log_messages())[0].messagetype == LogType.FeedRedirect.name + + +@pytest.mark.parametrize("refreshable_podcast_fixture", ["testdata/feed_entries_not_ordered"], indirect=True) +def test_podcastlist_refresh(refreshable_podcast_fixture): + p, episodes = refreshable_podcast_fixture + plist = PodcastList() + assert ilen(p.get_entries()) == 1 + assert plist.get_podcast_count() == 1 + list(plist.refresh(0)) + assert ilen(p.get_entries()) == 2 diff --git a/test/test_queue.py b/test/test_queue.py index 44b63010cc268989200d9bd0b0263609462b6d15..3682ea9841b1dec69d164c6224c84d7ca9c8c5ef 100644 --- a/test/test_queue.py +++ b/test/test_queue.py @@ -116,11 +116,6 @@ def test_queue_favorites(): for id in podcast.entry_ids_old_to_new: queue.insert_bottom(id) assert len(queue.podposts) == 20 - assert len(favorite.get_queue_favorites('https://wrong.url')) == 0 - assert [fav.id for fav in favorite.get_queue_favorites()] == [podcast.entry_ids_old_to_new[i] for i in - range(0, 10, 2)] - assert len(favorite.get_queue_favorites(url)) == 5 for id in podcast.entry_ids_old_to_new: queue.remove(id) assert len(queue.podposts) == 0 - assert len(favorite.get_queue_favorites()) == 0 diff --git a/test/test_search_dictparser_filters.py b/test/test_search_dictparser_filters.py new file mode 100644 index 0000000000000000000000000000000000000000..c30e819dec59369a4e275db4b9458381cee33a18 --- /dev/null +++ b/test/test_search_dictparser_filters.py @@ -0,0 +1,24 @@ +from podcast.podcast import Podcast +from podcast.search.DictParser import * + + +def test_empty_filter(): + assert create_filter_from_options({}) == None + + +def test_single_filters(): + dummy_pod = Podcast() + assert create_filter_from_options({"searchtext": "abc"}) == PodpostTextFilter("abc") + + assert create_filter_from_options({"fav_filter": True}) == FavoriteFilter(True) + assert create_filter_from_options({"fav_filter": False}) == FavoriteFilter(False) + + assert create_filter_from_options({"listened_filter": True}) == PodpostListenedFilter(True) + assert create_filter_from_options({"listened_filter": False}) == PodpostListenedFilter(False) + + assert create_filter_from_options({"podcast_url": "abc"}, {"abc": dummy_pod}) == PodcastFilter(dummy_pod) + + +def test_combining_filters(): + assert create_filter_from_options({"searchtext": "abc", "fav_filter": True}) == AndFilter(PodpostTextFilter("abc"), + FavoriteFilter(True)) diff --git a/test/test_search_dictparser_orders.py b/test/test_search_dictparser_orders.py new file mode 100644 index 0000000000000000000000000000000000000000..2f0b0327af1f8e2901f95a39371e4bc0c2c9e93a --- /dev/null +++ b/test/test_search_dictparser_orders.py @@ -0,0 +1,31 @@ +from podcast.search.DictParser import * + + +def test_empty_options(): + assert create_filter_from_options({}) == None + assert create_order_from_options({}) == PodpostPublishedDateOrder(Direction.DESC) + + +def test_order_only(): + assert create_order_from_options({"order_dir": "desc"}) == PodpostPublishedDateOrder(Direction.DESC) + assert create_order_from_options({"order_dir": "asc"}) == PodpostPublishedDateOrder(Direction.ASC) + + +def test_order_published(): + assert create_order_from_options({"order": "published", "order_dir": "desc"}) == PodpostPublishedDateOrder( + Direction.DESC) + assert create_order_from_options({"order": "published", "order_dir": "asc"}) == PodpostPublishedDateOrder( + Direction.ASC) + + +def test_order_inserted(): + assert create_order_from_options({"order": "inserted", "order_dir": "desc"}) == PodpostListInsertDateOrder( + Direction.DESC) + assert create_order_from_options({"order": "inserted", "order_dir": "asc"}) == PodpostListInsertDateOrder( + Direction.ASC) + + +def test_real_case(): + options = {'fav_filter': False, 'order': 'published', 'order_dir': 'asc', + 'podcast_url': 'https://augenzu.podigee.io/feed/ogg', 'searchtext': ''} + assert create_order_from_options(options) == PodpostPublishedDateOrder(Direction.ASC) diff --git a/test/test_searchbase.py b/test/test_searchbase.py new file mode 100644 index 0000000000000000000000000000000000000000..faa9ad80165aeed0e890a4d5545d4fbd24d04523 --- /dev/null +++ b/test/test_searchbase.py @@ -0,0 +1,29 @@ +from podcast.search.Filter import * +from podcast.search.Order import * +from podcast.search.SearchBase import * +from test import setup_inbox_with_2_posts + + +def test_inbox_searchbase(): + entry1, entry2 = setup_inbox_with_2_posts() + assert [entry2.id, entry1.id] == [post.id for post in list(InboxSearchBase().iter_search(NoFilter(),NoOrder()))] + assert [entry2.id, entry1.id] == [post.id for post in list(InboxSearchBase().iter_search(PodcastFilter(entry1.podcast),NoOrder()))] + assert [] == [post.id for post in list(InboxSearchBase().iter_search(PodcastFilter(None),NoOrder()))] + assert [entry1.id, entry2.id] == [post.id for post in list(InboxSearchBase().iter_search(NoFilter(),PodpostListInsertDateOrder(Direction.ASC)))] + assert [entry1.id, entry2.id] == [post.id for post in list(InboxSearchBase().iter_search(PodcastFilter(entry1.podcast),PodpostListInsertDateOrder(Direction.ASC)))] + assert [entry2.id, entry1.id] == [post.id for post in list(InboxSearchBase().iter_search(NoFilter(),PodpostListInsertDateOrder(Direction.DESC)))] + assert [entry2.id, entry1.id] == [post.id for post in list(InboxSearchBase().iter_search(NoFilter(),PodpostPublishedDateOrder(Direction.DESC)))] + assert [entry1.id, entry2.id] == [post.id for post in list(InboxSearchBase().iter_search(NoFilter(),PodpostPublishedDateOrder(Direction.ASC)))] + +def test_title_filter(): + entry1, entry2 = setup_inbox_with_2_posts() + assert [entry1.id] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Milch"), NoOrder()))] + assert [entry2.id] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Alien"), NoOrder()))] + assert [entry2.id] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("alien"), NoOrder()))] + assert [entry2.id] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Alien Wave"), NoOrder()))] + assert [] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Alien not Wave"), NoOrder()))] + assert [] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Wave Alien"), NoOrder()))] + assert [] == [post.id for post in list(AllSearchBase().iter_search(PodpostTextFilter("Krautsalat"), NoOrder()))] + assert [entry2.id] == [post.id for post in list(AllSearchBase().iter_search(AndFilter(PodpostTextFilter("Alien"), PodpostTextFilter("Wave")), NoOrder()))] + assert [entry1.id, entry2.id] == [post.id for post in list(AllSearchBase().iter_search(OrFilter(PodpostTextFilter("Alien"), PodpostTextFilter("Milch")), PodpostPublishedDateOrder(Direction.ASC)))] + assert [] == [post.id for post in list(AllSearchBase().iter_search(AndFilter(PodpostTextFilter("Alien"), PodpostTextFilter("Milch")), NoOrder()))] diff --git a/test/testdata/feed_entries_not_ordered.xml b/test/testdata/feed_entries_not_ordered.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e1f510e38eab582929984e0e707631784b80404 --- /dev/null +++ b/test/testdata/feed_entries_not_ordered.xml @@ -0,0 +1,70 @@ + + + + fakefeed + http://fakefeed.com + Tue, 02 Mar 2021 24:20:08 -0000 + Tue, 02 Mar 2021 23:20:11 -0000 + 60 + en-us + No Copyright © 2021 Podqast. No rights reserved. + no, none + + + + Richard + Test + thigg + http://blogs.law.harvard.edu/tech/rss + + https://example.com/image.png + imagetitle + imagelink.com + 1337 + 1337 + + + thigg + episodic + + + + + true + + nomail@example.com + bunny nunny + + subtitle + summary + + No Copyright © 2021 podqast. No Rights reserved. + + Arts + + prx_96_3554d3a0-e636-445f-b379-f1337deadbeef + Hello, I am a fake episode for testing!!öäü + Tue, 03 Mar 2021 23:20:08 -0000 + https://fakefeed.com/episode/1 + + + + I am a fake itunes subtitle + true + full + 1 + 13:37 + + + thigg + + + + + + + + + \ No newline at end of file diff --git a/test/testdata/feed_entries_not_ordered2.xml b/test/testdata/feed_entries_not_ordered2.xml new file mode 100644 index 0000000000000000000000000000000000000000..f8ae608e748053f04989cbee219ef067afb9e76b --- /dev/null +++ b/test/testdata/feed_entries_not_ordered2.xml @@ -0,0 +1,93 @@ + + + + fakefeed + http://fakefeed.com + Tue, 04 Mar 2021 24:20:08 -0000 + Tue, 04 Mar 2021 23:20:11 -0000 + 60 + en-us + No Copyright © 2021 Podqast. No rights reserved. + no, none + + + + Richard + Test + thigg + http://blogs.law.harvard.edu/tech/rss + + https://example.com/image.png + imagetitle + imagelink.com + 1337 + 1337 + + + thigg + episodic + + + + + true + + nomail@example.com + bunny nunny + + subtitle + summary + + No Copyright © 2021 podqast. No Rights reserved. + + Arts + + prx_96_3554d3a0-e636-445f-b379-f1337deadbeef + Hello, I am a fake episode for testing!!öäü + Tue, 03 Mar 2021 23:20:08 -0000 + https://fakefeed.com/episode/1 + + + + I am a fake itunes subtitle + true + full + 1 + 13:37 + + + thigg + + + + + + + + + prx_96_3554d3a0-e636-445f-b379-f1338deadbeef + Hello, I am the second fakefeed episode + Tue, 04 Mar 2021 23:20:08 -0000 + https://fakefeed.com/episode/1 + + + + I am a fake itunes subtitle + true + full + 1 + 13:37 + + + thigg + + + + + + + + + \ No newline at end of file diff --git a/test/testdata/new-feed-url-feed.rss b/test/testdata/new-feed-url-feed.rss new file mode 100644 index 0000000000000000000000000000000000000000..461e4009abbaafe1a4056470f6c0c03776c2e4bf --- /dev/null +++ b/test/testdata/new-feed-url-feed.rss @@ -0,0 +1,6 @@ + + + + https://newhoster.com/feed/ogg + + \ No newline at end of file diff --git a/translations/harbour-podqast-de.ts b/translations/harbour-podqast-de.ts index 8bd28180b4170c559b46ab04933ae38ff9045238..0fbc614c69ea3b6f73186e5cd9ab1dccca1a45b4 100644 --- a/translations/harbour-podqast-de.ts +++ b/translations/harbour-podqast-de.ts @@ -40,32 +40,32 @@ Archive - + Library Bibliothek - + History Verlauf - + Favorites Favoriten - + External Audio Fremde Audiodateien - + Rendering Zeichne - + Collecting Podcasts Sammle Podcasts @@ -388,12 +388,12 @@ Verlauf - + Rendering Zeichne - + Collecting Posts Sammle Beiträge @@ -401,22 +401,22 @@ Inbox - + Inbox Eingang - + Moving all posts to archive Lege alle Beiträge ins Archiv - + No new posts Keine neuen Beiträge - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist Menü um neue Podcasts zu Entdecken, hole Beiträge von der Bibliothek oder spiele die Playlist @@ -429,59 +429,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate Abspielgeschwindigkeit - + Sleep timer Schlummer-Uhr - + running... läuft... - + chapters Kapitel + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -531,42 +597,57 @@ PodcastSettings - - Posts - Beiträge + + Settings + Einstellungen + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to Neuen Beitrag - + Inbox Eingang - + Top of Playlist an den Anfang der Playlist - + Bottom of Playlist ans Ende der Playlist - + Archive in das Archiv - + Automatic post limit Höchstgrenze automatisierter Beiträge - + Audio playrate Abspielgeschwindigkeit @@ -582,12 +663,12 @@ PodpostList - + Rendering Zeichne - + Creating items Erzeuge Elemente @@ -603,17 +684,17 @@ PostListItem - + playing - + listened - + remaining @@ -673,13 +754,13 @@ - + Inbox in den Eingang - + Archive in das Archiv @@ -709,29 +790,29 @@ - + Download/Streaming Laden/Stream - + Development Entwicklung - + Experimental features Experimentelle Funktionen - + Top of Playlist an den Anfang der Playlist - + Bottom of Playlist ans Ende der Playlist @@ -756,61 +837,66 @@ min - + External Audio Fremde Audiodateien - + Allow external audio Erlaube fremde Audiodateien - + Will take data from Hole Daten von - + Move external audio to Externe Audiodateien - + Download Playlist Posts Herunterladen von Playlist-Beiträgen - + Download on Mobile Bei Mobilfunk laden - + Keep Favorites downloaded Favoriten speichern - + Will save data in Speicher Daten in - + System System - + Audio viewable in system Audio im System sichbar - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -960,13 +1046,13 @@ Podcasts vom OPML importiert - + Error Fehler - - + + Audio File not existing Audiodatei existiert nicht diff --git a/translations/harbour-podqast-es.ts b/translations/harbour-podqast-es.ts index a9d4bc11dcc420832d8e278b54bb054fafabb4b9..9186355232553e8737e9631b7cafaec86618eeed 100644 --- a/translations/harbour-podqast-es.ts +++ b/translations/harbour-podqast-es.ts @@ -40,32 +40,32 @@ Archive - + Library Biblioteca - + History Historial - + Favorites Favoritos - + External Audio Audio externo - + Rendering Generando - + Collecting Podcasts Obteniendo podcasts @@ -388,12 +388,12 @@ Historial - + Rendering Generando - + Collecting Posts Recopilando publicaciones @@ -401,22 +401,22 @@ Inbox - + Inbox Entradas - + Moving all posts to archive Mover todas las publicaciones al almacén - + No new posts No hay nuevas publicaciones - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist Desliza hacia abajo para encontrar nuevos podcasts, ver publicaciones de la biblioteca, o reproducir la lista de reproducción @@ -429,59 +429,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate Velocidad de reproducción - + Sleep timer - + running... - + chapters capítulos + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -531,42 +597,57 @@ PodcastSettings - - Posts - Publicaciones + + Settings + Ajustes + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to Mover nueva publicación a - + Inbox Entradas - + Top of Playlist Al principio de la lista - + Bottom of Playlist Al final de la lista - + Archive Almacén - + Automatic post limit Límite de publicaciones automáticas - + Audio playrate Velocidad de reproducción @@ -582,12 +663,12 @@ PodpostList - + Rendering Generando - + Creating items Creando elementos @@ -603,17 +684,17 @@ PostListItem - + playing - + listened - + remaining @@ -673,13 +754,13 @@ - + Inbox Entradas - + Archive Almacén @@ -709,29 +790,29 @@ - + Download/Streaming Descarga/Transmisión - + Development Desarrollo - + Experimental features Funciones experimentales - + Top of Playlist Al principio de la lista - + Bottom of Playlist Al final de la lista @@ -756,59 +837,64 @@ - + External Audio Audio externo - + Allow external audio Permitir audio externo - + Will take data from Cogerá datos de - + Move external audio to Mover audio externo a - + Download Playlist Posts Descargar publicaciones de la lista - + Download on Mobile Descargar en el móvil - + Keep Favorites downloaded Conservar favoritos descargados - + Will save data in Guardará los datos en - + System Sistema - + Audio viewable in system Audio visible en el sistema - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -958,13 +1044,13 @@ podcasts importados desde OPML - + Error Error - - + + Audio File not existing El archivo de audio no existe diff --git a/translations/harbour-podqast-fr.ts b/translations/harbour-podqast-fr.ts index cfa670d70182f6b47b81606b66c4967ec994ec34..f59f959d5184d5f8cecc42a642f944717984eb49 100644 --- a/translations/harbour-podqast-fr.ts +++ b/translations/harbour-podqast-fr.ts @@ -40,32 +40,32 @@ Archive - + Library Bibliothèque - + History Historique - + Favorites Favoris - + External Audio Fichiers Audios Externes - + Rendering Chargement - + Collecting Podcasts Récupération des Podcasts @@ -389,12 +389,12 @@ Historique - + Rendering Chargement - + Collecting Posts Récupération des Articles @@ -402,22 +402,22 @@ Inbox - + Inbox Boîte aux lettres - + Moving all posts to archive Transfert des courriers aux archives - + No new posts Aucun nouveau courrier - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist Glisser vers le bas pour Découvrir de nouveaux podcasts, obtenir de nouveaux courriers de la Bibliothèque, ou écouter la Playlist @@ -430,59 +430,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate Vitesse de lecture - + Sleep timer Minuterie avant coupure - + running... activée... - + chapters chapitres + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -532,42 +598,57 @@ PodcastSettings - - Posts - Articles + + Settings + Paramètres + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to Transférer les nouveaux articles - + Inbox Dans la Boîte aux lettres - + Top of Playlist En début de Playlist - + Bottom of Playlist En fin de playlist - + Archive Aux Archives - + Automatic post limit Limite automatique du nombre d'articles - + Audio playrate Vitesse de lecture @@ -583,12 +664,12 @@ PodpostList - + Rendering Chargement - + Creating items Génération des éléments @@ -604,17 +685,17 @@ PostListItem - + playing - + listened - + remaining @@ -674,13 +755,13 @@ - + Inbox Boîte aux lettres - + Archive Archives @@ -710,29 +791,29 @@ - + Download/Streaming Téléchargement/Streaming - + Development Développement - + Experimental features Fonctionnalités expérimentales - + Top of Playlist En début de Playlist - + Bottom of Playlist En fin de la Playlist @@ -757,61 +838,66 @@ - + External Audio Fichiers Audios Externes - + Allow external audio Autoriser les fichiers externes - + Will take data from Prendra les données depuis - + Move external audio to Transférer les fichiers externes - + Download Playlist Posts Télécharger les Playlists d'Articles - + Download on Mobile Télécharger hors Wifi - + Keep Favorites downloaded Conserver les favoris déjà téléchargés - + Will save data in Sauvegardera les données dans - + System Système - + Audio viewable in system Audio visible dans le système - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -961,13 +1047,13 @@ Podcasts importés depuis OPML - + Error Erreur - - + + Audio File not existing Le fichier audio n'existe pas diff --git a/translations/harbour-podqast-sv.ts b/translations/harbour-podqast-sv.ts index 87914dc5b738a4265056f9e2bfb9cc4c81aa6ac4..7e5f84f483eaa7b1786aef98cac5adfb7d3c9984 100644 --- a/translations/harbour-podqast-sv.ts +++ b/translations/harbour-podqast-sv.ts @@ -40,32 +40,32 @@ Archive - + Library Bibliotek - + History Historik - + Favorites Favoriter - + External Audio Externt ljud - + Rendering Rendering - + Collecting Podcasts Hämtar poddar @@ -388,12 +388,12 @@ Historik - + Rendering Rendering - + Collecting Posts Hämtar poster @@ -401,22 +401,22 @@ Inbox - + Inbox Inkorg - + Moving all posts to archive Flyttar alla poster till arkiv - + No new posts Inga nya poster - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist Dra neråt för att upptäcka nya poddar, hämta poster från biblioteket eller spela upp spelningslistan. @@ -429,59 +429,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate Spelningsfrekvens - + Sleep timer Insomningsur - + running... körs... - + chapters avsnitt + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -531,42 +597,57 @@ PodcastSettings - - Posts - Poster + + Settings + Inställningar + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to Flytta ny post till - + Inbox Inkorg - + Top of Playlist Först i spelningslistan - + Bottom of Playlist Sist i spelningslistan - + Archive Arkiv - + Automatic post limit Automatisk postbegränsning - + Audio playrate Spelningsfrekvens @@ -582,12 +663,12 @@ PodpostList - + Rendering Rendering - + Creating items Skapar objekt @@ -603,17 +684,17 @@ PostListItem - + playing - + listened - + remaining @@ -673,13 +754,13 @@ - + Inbox Inkorg - + Archive Arkiv @@ -709,29 +790,29 @@ - + Download/Streaming Nerladdning/Strömmning - + Development Utveckling - + Experimental features Experimentella funktioner - + Top of Playlist Först i spelningslistan - + Bottom of Playlist Sist i spelningslistan @@ -756,61 +837,66 @@ min - + External Audio Externt ljud - + Allow external audio Tillåt externt ljud - + Will take data from Hämtar data från - + Move external audio to Flytta externt ljud till - + Download Playlist Posts Ladda ner spelningslistans poster - + Download on Mobile Ladda ner på mobilanslutning - + Keep Favorites downloaded Behåll favoriter nerladdade - + Will save data in Sparar data i - + System System - + Audio viewable in system Ljud som visas i systemet - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -960,13 +1046,13 @@ poddar importerade från OPML - + Error Fel - - + + Audio File not existing Ljudfilen finns inte diff --git a/translations/harbour-podqast-zh_CN.ts b/translations/harbour-podqast-zh_CN.ts index d73fc4c2c7242e27ce69c79e22e13c0e13dceca7..eb9a6a2acb94ef43c4fc25824c63a395897ac527 100644 --- a/translations/harbour-podqast-zh_CN.ts +++ b/translations/harbour-podqast-zh_CN.ts @@ -40,32 +40,32 @@ Archive - + Library - + History 历史 - + Favorites 收藏 - + External Audio 外部音频 - + Rendering 生成 - + Collecting Podcasts 收藏的播客 @@ -388,12 +388,12 @@ 历史 - + Rendering 生成 - + Collecting Posts 收藏内容 @@ -401,22 +401,22 @@ Inbox - + Inbox 收件箱 - + Moving all posts to archive 移动所有内容到存档 - + No new posts 无新内容 - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist 下拉以找到新播客、从库获取内容或播放播放列表中的内容 @@ -429,59 +429,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate 音频播放率 - + Sleep timer 睡眠计时器 - + running... 运行中…… - + chapters 章节 + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -531,42 +597,57 @@ PodcastSettings - - Posts - 内容 + + Settings + 设置 + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to 移动新内容到 - + Inbox 收件箱 - + Top of Playlist 播放列表顶部 - + Bottom of Playlist 播放列表底部 - + Archive 存档 - + Automatic post limit 自动限制内容 - + Audio playrate 音频播放率 @@ -582,12 +663,12 @@ PodpostList - + Rendering 更新 - + Creating items 创建新项目 @@ -603,17 +684,17 @@ PostListItem - + playing - + listened - + remaining @@ -673,13 +754,13 @@ - + Inbox 收件箱 - + Archive 存档 @@ -709,29 +790,29 @@ - + Download/Streaming 下载流媒体 - + Development 开发 - + Experimental features 实验功能 - + Top of Playlist 播放列表顶部 - + Bottom of Playlist 播放列表底部 @@ -756,59 +837,64 @@ 分钟 - + External Audio 外部音频 - + Allow external audio 允许外部音频 - + Will take data from 将会获取数据自 - + Move external audio to 移动外部音频到 - + Download Playlist Posts 下载播放列表内容 - + Download on Mobile 通过移动网络下载 - + Keep Favorites downloaded 保留下载的收藏 - + Will save data in 将会保存数据到 - + System 系统 - + Audio viewable in system 音频在系统可见 - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -958,13 +1044,13 @@ 已从 OPML 导入播客 - + Error 错误 - - + + Audio File not existing 音频文件不存在 diff --git a/translations/harbour-podqast.ts b/translations/harbour-podqast.ts index 1274bf80681bd64860e6cbae01d401ffe02c6c8c..f9b5f40ec607450ed546c200002ebf76d3d1837f 100644 --- a/translations/harbour-podqast.ts +++ b/translations/harbour-podqast.ts @@ -40,32 +40,32 @@ Archive - + Library - + History History - + Favorites Favorites - + External Audio External Audio - + Rendering Rendering - + Collecting Podcasts Collecting Podcasts @@ -388,12 +388,12 @@ History - + Rendering Rendering - + Collecting Posts Collecting Posts @@ -401,22 +401,22 @@ Inbox - + Inbox Inbox - + Moving all posts to archive Moving all posts to archive - + No new posts No new posts - + Pull down to Discover new podcasts, get posts from Library, or play the Playlist Pull down to Discover new podcasts, get posts from Library, or play the Playlist @@ -429,59 +429,125 @@ - + No logs - + Skipped refresh on '%1', because the server responded with 'Not Modified' - + Updated '%1'. %2 new episodes - + '%1' could not be refreshed because: %2 + + MainPageUsageHint + + + You can see whats going on by looking into the log down here + + + + + Podqast supports pagination of feeds. If you are missing old episodes of feeds, you can find a force refresh button in the settings. + + + + + On an episode image, this icon shows that it is downloaded... + + + + + Hi there, I gonna give you a quick tour to show you basic and new features. + + + + + Quick touch on an episode to see its description, long press it to add it to queue or play it + + + + + ...currently playing... + + + + + ...already listened. + + + + + In the dock below, this icon means the file currently played is streaming from the internet right now... + + + + + ...the sleeptimer is active + + + + + ...a playrate different from 1 is set. + + + Player - + Auto-play next episode in queue - + Stop after each episode - + Audio playrate Audio playrate - + Sleep timer Sleep timer - + running... running... - + chapters chapters + + PlayerHint + + + Select if the next item of the playlist should be played automatically + + + + + Click the image to adjust the playrate + + + PodcastDirectorySearchPage @@ -531,42 +597,57 @@ PodcastSettings - - Posts - Posts + + Settings + Settings + + + + URL + + + + + Link + + + + + %1 Posts + - + Move new post to Move new post to - + Inbox Inbox - + Top of Playlist Top of Playlist - + Bottom of Playlist Bottom of Playlist - + Archive Archive - + Automatic post limit Automatic post limit - + Audio playrate Audio playrate @@ -582,12 +663,12 @@ PodpostList - + Rendering Rendering - + Creating items Creating items @@ -603,17 +684,17 @@ PostListItem - + playing - + listened - + remaining @@ -673,13 +754,13 @@ - + Inbox Inbox - + Archive Archive @@ -709,29 +790,29 @@ - + Download/Streaming Download/Streaming - + Development Development - + Experimental features Experimental features - + Top of Playlist Top of Playlist - + Bottom of Playlist Bottom of Playlist @@ -756,61 +837,66 @@ min - + External Audio External Audio - + Allow external audio Allow external audio - + Will take data from Will take data from - + Move external audio to Move external audio to - + Download Playlist Posts Download Playlist Posts - + Download on Mobile Download on Mobile - + Keep Favorites downloaded Keep Favorites downloaded - + Will save data in Will save data in - + System System - + Audio viewable in system Audio viewable in system - + + Auto-play next episode in queue + + + + Force refresh of old episodes @@ -960,13 +1046,13 @@ Podcasts imported from OPML - + Error Error - - + + Audio File not existing Audio File not existing