From a48bc363aa9c56009e2be86c7fe14fc77f7559ba Mon Sep 17 00:00:00 2001 From: lekma Date: Mon, 14 Oct 2024 19:03:23 +0100 Subject: [PATCH] v3.2.5 --- addon.xml | 16 ++- lib/invidious/client.py | 36 ++++-- lib/invidious/extract.py | 42 ++----- lib/invidious/feed.py | 25 +++-- lib/invidious/folders.py | 4 + lib/invidious/instance.py | 46 ++++++-- lib/invidious/items.py | 13 ++- lib/invidious/persistence.py | 45 +++++++- lib/invidious/search.py | 10 +- lib/invidious/session.py | 19 ++-- lib/invidious/utils.py | 39 +++++-- lib/plugin.py | 106 ++++++++++-------- .../resource.language.en_gb/strings.po | 6 +- resources/media/more.png | Bin 3559 -> 0 bytes resources/media/next.png | Bin 0 -> 1822 bytes resources/media/previous.png | Bin 0 -> 1815 bytes resources/settings.xml | 8 +- 17 files changed, 257 insertions(+), 158 deletions(-) delete mode 100644 resources/media/more.png create mode 100644 resources/media/next.png create mode 100644 resources/media/previous.png diff --git a/addon.xml b/addon.xml index d8ceff7..a6a81fd 100644 --- a/addon.xml +++ b/addon.xml @@ -1,19 +1,19 @@ - - + + - - + + - + @@ -22,9 +22,7 @@ - - executable - + true diff --git a/lib/invidious/client.py b/lib/invidious/client.py index 64b4c4b..a7c3557 100644 --- a/lib/invidious/client.py +++ b/lib/invidious/client.py @@ -10,6 +10,7 @@ from invidious.items import ( FeedChannels, Folders, Playlists, Queries, Results, Video, Videos ) +from invidious.persistence import IVNavigationHistory # instance --------------------------------------------------------------------- @@ -36,6 +37,7 @@ class IVClient(object): def __init__(self, logger): self.logger = logger.getLogger(f"{logger.component}.client") self.__client__ = Client() + self.__history__ = IVNavigationHistory() # video -------------------------------------------------------------------- @@ -57,29 +59,35 @@ def tabs(self, **kwargs): @instance def tab(self, key, **kwargs): - if (videos := self.__client__.instance.tab(key, **kwargs)): + items, next, category = self.__client__.instance.tab(key, **kwargs) + if items is not None: + previous = self.__history__.continuation( + key, kwargs.get("continuation") + ) return Videos( - videos["items"], - continuation=videos["continuation"], - category=videos["channel"] + items, next=next, category=category, previous=previous ) @instance def playlists(self, **kwargs): - if (playlists := self.__client__.instance.playlists(**kwargs)): + items, next, category = self.__client__.instance.playlists(**kwargs) + if items is not None: + previous = self.__history__.continuation( + "playlists", kwargs.get("continuation") + ) return Playlists( - playlists["items"], - continuation=playlists["continuation"], - category=playlists["channel"] + items, next=next, category=category, previous=previous ) # playlist ----------------------------------------------------------------- @instance def playlist(self, **kwargs): - if(playlist := self.__client__.instance.playlist(**kwargs)): + items, next, category = self.__client__.instance.playlist(**kwargs) + if items is not None: + previous = self.__history__.index("playlist", kwargs["index"]) return Videos( - playlist["items"], limit=200, category=playlist["title"] + items, next=next, category=category, previous=previous ) # home --------------------------------------------------------------------- @@ -91,7 +99,9 @@ def home(self): @instance def feed(self, limit=29, **kwargs): - return Videos(self.__client__.feed.feed(limit, **kwargs), limit=limit) + items, next = self.__client__.feed.feed(limit, **kwargs) + previous = self.__history__.page("feed", kwargs["page"]) + return Videos(items, next=next, previous=previous) @instance def channels(self): @@ -123,6 +133,8 @@ def history(self): @instance def search(self, query): + items, next = self.__client__.search.search(query) + previous = self.__history__.page("search", query["page"]) return Results( - self.__client__.search.search(query), limit=20, category=query["q"] + items, next=next, previous=previous, category=query["q"] ) diff --git a/lib/invidious/extract.py b/lib/invidious/extract.py index 7c5a7f8..ea538f5 100644 --- a/lib/invidious/extract.py +++ b/lib/invidious/extract.py @@ -206,13 +206,14 @@ def __init__(self, item, expires=1800): # ------------------------------------------------------------------------------ -# IVPlaylistVideos +# IVPlaylists -class IVPlaylistVideos(IVPlaylist): +class IVPlaylists(list): - def __init__(self, item): - super(IVPlaylistVideos, self).__init__(item) - self["items"] = IVVideos(item["videos"]) + def __init__(self, playlists): + super(IVPlaylists, self).__init__( + IVPlaylist(playlist) for playlist in playlists if playlist + ) # ------------------------------------------------------------------------------ @@ -250,7 +251,7 @@ def __init__(self, item, expires=1800): subscribers=subscribers, subscribersText=subscribersText, tabs=[ - dict(tab, action=action) + dict(tab, action=action, properties={"SpecialSort": "top"}) for action, tab in self.__tabs__.items() if action in item.get("tabs", []) ] @@ -261,35 +262,6 @@ def __repr__(self): return f"IVChannel({self['channel']})" -# ------------------------------------------------------------------------------ -# IVChannelVideos - -class IVChannelVideos(dict): - - def __init__(self, channel, items): - super(IVChannelVideos, self).__init__( - channel=channel, - continuation=items.get("continuation"), - items=IVVideos(items["videos"]) - ) - - -# ------------------------------------------------------------------------------ -# IVChannelPlaylists - -class IVChannelPlaylists(dict): - - def __init__(self, channel, items): - super(IVChannelPlaylists, self).__init__( - channel=channel, - continuation=items.get("continuation"), - items=[ - IVPlaylist(playlist) - for playlist in items["playlists"] if playlist - ] - ) - - # ------------------------------------------------------------------------------ # IVResults diff --git a/lib/invidious/feed.py b/lib/invidious/feed.py index d412f6f..12fcf8e 100644 --- a/lib/invidious/feed.py +++ b/lib/invidious/feed.py @@ -52,22 +52,25 @@ def update(self, videos): self.__last__ = time() def page(self, limit, page): - return self.__videos__[(limit * (page - 1)):(limit * page)] + videos, next = ( + self.__videos__[:(fence := (limit * page))], + self.__videos__[fence:] + ) + return ( + videos[(limit * (page - 1)):], + {"page": (page + 1)} if next else None + ) # feed --------------------------------------------------------------------- @public def feed(self, limit, page=1): - t = time() - try: - if ( - ((page := int(page)) == 1) and - ((keys := self.invalid()) is not None) - ): - self.update(self.__instance__.__feeds__(keys)) - return self.page(limit, page) - finally: - self.logger.info(f"feed() took: {time() - t} seconds") + if ( + ((page := int(page)) == 1) and + ((keys := self.invalid()) is not None) + ): + self.update(self.__instance__.__feeds__(keys)) + return self.page(limit, page) # channels ----------------------------------------------------------------- diff --git a/lib/invidious/folders.py b/lib/invidious/folders.py index ba1d03c..ca12281 100644 --- a/lib/invidious/folders.py +++ b/lib/invidious/folders.py @@ -27,6 +27,7 @@ "action": "trending", "title": 30310, "art": "DefaultAddonMusic.png", + "properties": {"SpecialSort": "top"}, "kwargs": { "type": "music", "category": localizedString(30310) @@ -36,6 +37,7 @@ "action": "trending", "title": 30320, "art": "DefaultAddonGame.png", + "properties": {"SpecialSort": "top"}, "kwargs": { "type": "gaming", "category": localizedString(30320) @@ -45,6 +47,7 @@ "action": "trending", "title": 30330, "art": "DefaultMovies.png", + "properties": {"SpecialSort": "top"}, "kwargs": { "type": "movies", "category": localizedString(30330) @@ -68,6 +71,7 @@ def __init__(self, action, folder): action=folder.get("action", action), setting=folder.get("setting"), art=dict.fromkeys(("poster", "icon"), folder.get("art")), + properties=folder.get("properties"), kwargs=folder.get("kwargs", {}) ) diff --git a/lib/invidious/instance.py b/lib/invidious/instance.py index abd13e6..ec36e68 100644 --- a/lib/invidious/instance.py +++ b/lib/invidious/instance.py @@ -13,9 +13,7 @@ ) from invidious.extract import ( - IVChannel, IVChannelVideos, IVChannelPlaylists, - IVPlaylist, IVPlaylistVideos, - IVResults, IVVideo, IVVideos + IVChannel, IVPlaylist, IVPlaylists, IVResults, IVVideo, IVVideos ) from invidious.regional import locales, regions from invidious.session import IVSession @@ -245,31 +243,59 @@ def tabs(self, **kwargs): self.logger.error(f"Invalid channelId: {channelId}", notify=True) def __tab__(self, channelId, key, **kwargs): + #if (("continuation" in kwargs) and (not kwargs["continuation"])): + # del kwargs["continuation"] + if (continuation := kwargs.pop("continuation", None)): + kwargs["continuation"] = continuation return ( - self.__channel__(channelId)["channel"], - self.__get__(key, channelId, **kwargs) + self.__get__(key, channelId, **kwargs), + self.__channel__(channelId)["channel"] ) @public def tab(self, key, **kwargs): if (channelId := kwargs.pop("channelId", None)): - return IVChannelVideos(*self.__tab__(channelId, key, **kwargs)) + items, channel = self.__tab__(channelId, key, **kwargs) + return ( + IVVideos(items["videos"]), + ( + {"continuation": continuation} + if (continuation := items.get("continuation")) else None + ), + channel + ) self.logger.error(f"Invalid channelId: {channelId}", notify=True) @public def playlists(self, **kwargs): if (channelId := kwargs.pop("channelId", None)): - return IVChannelPlaylists( - *self.__tab__(channelId, "playlists", **kwargs) + items, channel = self.__tab__(channelId, "playlists", **kwargs) + return ( + IVPlaylists(items["playlists"]), + ( + {"continuation": continuation} + if (continuation := items.get("continuation")) else None + ), + channel ) self.logger.error(f"Invalid channelId: {channelId}", notify=True) # playlist ----------------------------------------------------------------- @public - def playlist(self, **kwargs): + def playlist(self, limit=200, **kwargs): if (playlistId := kwargs.pop("playlistId", None)): - return IVPlaylistVideos(self.__get_playlist__(playlistId, **kwargs)) + items = ( + (item := self.__get_playlist__(playlistId, **kwargs))["videos"] + ) + return ( + IVVideos(items), + ( + {"index": (kwargs["index"] + count)} + if ((count := len(items)) >= limit) else None + ), + item["title"] + ) self.logger.error(f"Invalid playlistId: {playlistId}", notify=True) # feed --------------------------------------------------------------------- diff --git a/lib/invidious/items.py b/lib/invidious/items.py index 8fd32a8..6c27352 100644 --- a/lib/invidious/items.py +++ b/lib/invidious/items.py @@ -63,9 +63,10 @@ class Items(List): __ctor__ = Item - def __init__(self, items, continuation=None, limit=0, **kwargs): + def __init__(self, items, previous=None, next=None, **kwargs): super(Items, self).__init__(items, **kwargs) - self.more = continuation or ((len(self) >= limit) if limit else False) + self.previous = previous + self.next = next class Contents(Items): @@ -85,6 +86,10 @@ class Folder(Item): def action(self): return self.get("action", self.type) + @property + def properties(self): + return self.get("properties") + @property def plot(self): if (plot := self.get("plot")): @@ -102,6 +107,7 @@ def getItem(self, url, **kwargs): title, buildUrl(url, action=self.action, **dict(self.kwargs, **kwargs)), isFolder=True, + properties=self.properties, infoLabels=self.labels(title, self.plot or title), **self.art ) @@ -147,8 +153,7 @@ def getItem(self, url): action="search", q=self.q, type=self.type, - sort=self.sort, - page=self.page + sort=self.sort ), isFolder=True, infoLabels=self.labels(self.q, self.q), diff --git a/lib/invidious/persistence.py b/lib/invidious/persistence.py index af0f9cf..51bbfc7 100644 --- a/lib/invidious/persistence.py +++ b/lib/invidious/persistence.py @@ -6,6 +6,48 @@ from nuttig import save, Persistent +# ------------------------------------------------------------------------------ +# IVNavigationHistory + +class IVNavigationHistory(Persistent, dict): + + def __missing__(self, action): + self[action] = list() + return self[action] + + def __push__(self, action, key, value): + if ((item := {key: value}) in self[action]): + self[action] = self[action][:self[action].index(item)] + try: + return self[action][-1] + except IndexError: + return None + finally: + self[action].append(item) + + @save + def index(self, action, index): + if (index == 50): + self[action].clear() + return self.__push__(action, "index", index) + + @save + def page(self, action, page): + if (page == 1): + self[action].clear() + return self.__push__(action, "page", page) + + @save + def continuation(self, action, continuation): + if (not continuation): + self[action].clear() + return self.__push__(action, "continuation", continuation) + + @save + def clear(self): + super(IVNavigationHistory, self).clear() + + # ------------------------------------------------------------------------------ # IVSearchHistory @@ -25,8 +67,7 @@ def __init__(self, *args, **kwargs): { "q": q["query"], "type": q["type"], - "sort": qsort, - "page": 1 + "sort": qsort } ) diff --git a/lib/invidious/search.py b/lib/invidious/search.py index a3d1b71..393e7e6 100644 --- a/lib/invidious/search.py +++ b/lib/invidious/search.py @@ -103,8 +103,7 @@ def query(self, **query):# this is a trick! query = { "q": q, "type": self.__q_type__ or self.q_type(), - "sort": self.__q_sort__ or self.q_sort(), - "page": 1 + "sort": self.__q_sort__ or self.q_sort() } if self.__history__: self.__queries__.record(query) @@ -120,11 +119,14 @@ def history(self): # search ------------------------------------------------------------------- @public - def search(self, query): + def search(self, query, limit=20): self.__cache__.append(query) if self.__history__: self.__queries__.move_to_end(query["q"]) - return self.__instance__.search(query) + return ( + (items := self.__instance__.search(query)), + {"page": (query["page"] + 1)} if (len(items) >= limit) else None + ) # -------------------------------------------------------------------------- diff --git a/lib/invidious/session.py b/lib/invidious/session.py index 8ef805b..40361b4 100644 --- a/lib/invidious/session.py +++ b/lib/invidious/session.py @@ -3,7 +3,7 @@ from concurrent.futures import ThreadPoolExecutor -from requests import HTTPError, RequestException, Session +from requests import HTTPError, RequestException, Session, Timeout from nuttig import buildUrl, getSetting, localizedString @@ -44,7 +44,8 @@ def request(self, method, url, notify=True, **kwargs): ) except RequestException as error: self.logger.error(error, notify=notify) - raise error + if (not isinstance(error, Timeout)): + raise error # -------------------------------------------------------------------------- @@ -58,13 +59,13 @@ def __error__(self, result, notify=True): return (False, result) def __get__(self, url, notify=True, **kwargs): - response = self.get(url, notify=notify, params=kwargs) - notified, result = self.__error__(response.json(), notify=notify) - try: - response.raise_for_status() - except HTTPError as error: - self.logger.error(error, notify=((not notified) and notify)) - return result + if (response := self.get(url, notify=notify, params=kwargs)): + notified, result = self.__error__(response.json(), notify=notify) + try: + response.raise_for_status() + except HTTPError as error: + self.logger.error(error, notify=((not notified) and notify)) + return result def __map_get__(self, urls, **kwargs): def __pool_get__(url): diff --git a/lib/invidious/utils.py b/lib/invidious/utils.py index 7f3899c..86cb4a7 100644 --- a/lib/invidious/utils.py +++ b/lib/invidious/utils.py @@ -26,29 +26,50 @@ def __makeItem__(label, url, art=None, isFolder=True, properties=None, **kwargs) # settings item def settingsItem(url, **kwargs): return __makeItem__( - 5, url, art="DefaultAddonService.png", isFolder=False, **kwargs + 5, + url, + art="DefaultAddonService.png", + isFolder=False, + properties={"SpecialSort": "bottom"}, + **kwargs ) # newQuery item def newQueryItem(url, **kwargs): - return __makeItem__(30410, url, art="DefaultAddSource.png", **kwargs) + return __makeItem__( + 30410, + url, + art="DefaultAddSource.png", + properties={"SpecialSort": "top"}, + **kwargs + ) # channels item def channelsItem(url, **kwargs): - return __makeItem__(30110, url, art="DefaultArtist.png", **kwargs) + return __makeItem__( + 30110, + url, + art="DefaultArtist.png", + properties={"SpecialSort": "top"}, + **kwargs + ) -# more item -__more_art__ = getMedia("more") +# navigation item +__navigation_targets__ = { + "previous": (30001, getMedia("previous"), {"SpecialSort": "top"}), + "next": (30002, getMedia("next"), {"SpecialSort": "bottom"}) +} -def moreItem(url, **kwargs): +def navigationItem(target, url, **kwargs): + label, art, properties = __navigation_targets__[target] return __makeItem__( - 30001, + label, url, - art=__more_art__, - properties={"SpecialSort": "bottom"}, + art=art, + properties=properties, **kwargs ) diff --git a/lib/plugin.py b/lib/plugin.py index bbde494..101c490 100644 --- a/lib/plugin.py +++ b/lib/plugin.py @@ -2,7 +2,6 @@ from sys import argv -from time import time from urllib.parse import urlencode from inputstreamhelper import Helper @@ -10,7 +9,9 @@ from nuttig import action, getSetting, openSettings, parseQuery, Plugin from invidious.client import IVClient -from invidious.utils import channelsItem, moreItem, newQueryItem, settingsItem +from invidious.utils import ( + channelsItem, navigationItem, newQueryItem, settingsItem +) # ------------------------------------------------------------------------------ @@ -24,38 +25,39 @@ def __init__(self, *args, **kwargs): # helpers ------------------------------------------------------------------ - def addMore(self, more, count=0, **kwargs): - if more is True: - if (index := kwargs.get("index")): - if count: - kwargs["index"] = int(index) + count - else: - del kwargs["index"] - else: - kwargs["page"] = int(kwargs.get("page", 1)) + 1 - else: - kwargs["continuation"] = more - return self.addItem( - moreItem(self.url, action=self.action, **kwargs) - ) + def addChannels(self): + return self.addItem(channelsItem(self.url, action="channels")) - def addDirectory(self, items, *args, **kwargs): - if super(IVPlugin, self).addDirectory(items, *args): - if (more := getattr(items, "more", None)): - return self.addMore(more, count=len(items), **kwargs) - return True - return False + def addNewQuery(self): + return self.addItem(newQueryItem(self.url, action="search", new=True)) - def addSettingsItem(self): + def addSettings(self): if getSetting("home.settings", bool): return self.addItem(settingsItem(self.url, action="settings")) return True - def addNewQueryItem(self): - return self.addItem(newQueryItem(self.url, action="search", new=True)) + def addNavigation(self, target, items, **kwargs): + if (_kwargs_ := getattr(items, target, None)): + return self.addItem( + navigationItem( + target, + self.url, + action=self.__action__, + **dict(kwargs, **_kwargs_) + ) + ) + return True - def addChannelsItem(self): - return self.addItem(channelsItem(self.url, action="channels")) + def addDirectory(self, items, *args, **kwargs): + if ( + self.addNavigation("previous", items, **kwargs) and + super(IVPlugin, self).addDirectory(items, *args) and + self.addNavigation("next", items, **kwargs) + ): + return True + return False + + # play --------------------------------------------------------------------- def playItem( self, item, manifestType, mimeType=None, headers=None, params=None @@ -80,8 +82,6 @@ def playItem( return super(IVPlugin, self).playItem(item, mimeType=mimeType) return False - # play --------------------------------------------------------------------- - @action() def play(self, **kwargs): return self.playItem( @@ -94,9 +94,11 @@ def play(self, **kwargs): @action() def channel(self, **kwargs): + if ("continuation" in kwargs): + self.__updateListing__ = True if ( ( - (not ("continuation" in kwargs)) and + (not kwargs.get("continuation")) and (tabs := self.__client__.tabs(**kwargs)) and (not self.addItems(tabs)) ) or @@ -107,18 +109,24 @@ def channel(self, **kwargs): @action(category=31100) def playlists(self, **kwargs): + if ("continuation" in kwargs): + self.__updateListing__ = True if ((items := self.__client__.playlists(**kwargs)) is not None): return self.addDirectory(items, **kwargs) return False @action(category=31200) def streams(self, **kwargs): + if ("continuation" in kwargs): + self.__updateListing__ = True if ((items := self.__client__.tab("streams", **kwargs)) is not None): return self.addDirectory(items, **kwargs) return False @action(category=31300) def shorts(self, **kwargs): + if ("continuation" in kwargs): + self.__updateListing__ = True if ((items := self.__client__.tab("shorts", **kwargs)) is not None): return self.addDirectory(items, **kwargs) return False @@ -126,9 +134,12 @@ def shorts(self, **kwargs): # playlist ----------------------------------------------------------------- @action() - def playlist(self, index=50, **kwargs): - if ((items := self.__client__.playlist(index=index, **kwargs)) is not None): - return self.addDirectory(items, index=index, **kwargs) + def playlist(self, **kwargs): + if ("index" in kwargs): + self.__updateListing__ = True + kwargs["index"] = int(kwargs.get("index", 50)) + if ((items := self.__client__.playlist(**kwargs)) is not None): + return self.addDirectory(items, **kwargs) return False # home --------------------------------------------------------------------- @@ -136,26 +147,22 @@ def playlist(self, index=50, **kwargs): @action(category=30000) def home(self, **kwargs): if self.addDirectory(self.__client__.home()): - return self.addSettingsItem() + return self.addSettings() return False # feed --------------------------------------------------------------------- @action(category=30100, cacheToDisc=False) def feed(self, **kwargs): - t = time() - try: - if ( - ( - (int(kwargs.get("page", 1)) == 1) and - (not self.addChannelsItem()) - ) or - ((items := self.__client__.feed(**kwargs)) is None) - ): - return False - return self.addDirectory(items, **kwargs) - finally: - self.logger.info(f"feed() took: {time() - t} seconds") + if ("page" in kwargs): + self.__updateListing__ = True + page = kwargs["page"] = int(kwargs.get("page", 1)) + if ( + ((page == 1) and (not self.addChannels())) or + ((items := self.__client__.feed(**kwargs)) is None) + ): + return False + return self.addDirectory(items, **kwargs) @action(category=30111) @@ -190,11 +197,14 @@ def __query__(self): return self.__client__.query() def __history__(self): - if self.addNewQueryItem(): + if self.addNewQuery(): return self.addDirectory(self.__client__.history()) return False def __search__(self, query): + if ("page" in query): + self.__updateListing__ = True + query["page"] = int(query.get("page", 1)) if ((results := self.__client__.search(query)) is not None): return self.addDirectory(results, **query) return False diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 108e4ef..5643a3c 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -14,7 +14,11 @@ msgid "Invidious" msgstr "" msgctxt "#30001" -msgid "More..." +msgid "<< Previous" +msgstr "" + +msgctxt "#30002" +msgid "Next >>" msgstr "" diff --git a/resources/media/more.png b/resources/media/more.png deleted file mode 100644 index ce9fc25373ef8ade32ee7077b60e34790721b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3559 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 diff --git a/resources/media/next.png b/resources/media/next.png new file mode 100644 index 0000000000000000000000000000000000000000..44d5ff302df3d13a8f4953da946d94356c06d6c5 GIT binary patch literal 1822 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 literal 0 HcmV?d00001 diff --git a/resources/settings.xml b/resources/settings.xml index ed53e24..de3c60e 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -201,7 +201,7 @@ - + 0 @@ -247,7 +247,7 @@ - +