diff --git a/plugin.video.invidious/LICENSE.txt b/plugin.video.invidious/LICENSE.txt new file mode 100644 index 000000000..56e3f0c51 --- /dev/null +++ b/plugin.video.invidious/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2020 TheAssassin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin.video.invidious/README.md b/plugin.video.invidious/README.md new file mode 100644 index 000000000..42b2ff4e5 --- /dev/null +++ b/plugin.video.invidious/README.md @@ -0,0 +1,45 @@ +# [Invidious](https://invidio.us) plugin for [Kodi](https://kodi.tv) + +This plugin provides an [Invidious](https://invidio.us) client for [Kodi](https://kodi.tv). Invidious is a privacy-friendly web frontend to YouTube. + +**Note:** Currently, this plugin is only tested with Kodi 20 and newer. + +## Installation + +To keep track with development, it is recommended to install the plugin with git: + +```shell script +# please change the destination if necessary +git clone https://github.com/petterreinholdtsen/kodi-invidious-plugin.git ~/.kodi/addons/plugin.video.invidious + +# on an embedded device (e.g., an X96 Mini running CoreELEC, you need to clone to /storage/.kodi/addons/plugin.video.invidious +``` + +You can also download an archive and extract it in the right place instead of using git: + +```shell script +# same here: make sure you change to the right directory +cd ~/.kodi/addons/ + +# download a zip archive +mkdir plugin.video.invidious +wget https://github.com/petterreinholdtsen/kodi-invidious-plugin/archive/master.tar.gz -O - | tar xz --strip-components=1 -C plugin.video.invidious +``` + +It is strongly recommended to use git instead of an archive, as it makes updating the plugin a lot easier. + +You may have to restart Kodi before you can enable the plugin. To enable the plugin, please go to the addons settings, switch to *user plugins* and enable the plugin there. + + +## To Do + +- evaluate using youtube-dl to remove dependency on third-party web service (maybe in a second plugin) +- implement adaptive streaming properly +- be able to open YouTube videos from [NewPipe](https://newpipe.net) +- ~~add support for settings, e.g., to configure Invidious instance~~ +- ~~support for adaptive streaming~~ +- consider supporting versions < Kodi 18 (e.g., by making the dependency on inputstream-helper optional) +- ~~*trending*, *top*, *popular* etc. video lists (not too important to most people, but Invidious offers endpoints, so why not?)~~ +- visit channel of list items (e.g., via the context menu of a video search result) +- save channels in favorite list (for quick access from the main menu) +- support for subtitles diff --git a/plugin.video.invidious/addon.xml b/plugin.video.invidious/addon.xml new file mode 100644 index 000000000..c3927da48 --- /dev/null +++ b/plugin.video.invidious/addon.xml @@ -0,0 +1,32 @@ + + + + + + + + + + video + + + + + Invidious client + A privacy-friendly way of watching YouTube content. Uses the great Invidious web service's API to do the heavy lifting. + This plugin is not endorsed by Google + Invidious-klient + En personvernvennlig måte å se YouTube-innhold på. Bruker den flotte Invidious-netttjenestens API for å gjøre det tunge arbeidet. + Denne plugin-modulen er ikke godkjent av Google. + en nb + all + MIT + https://github.com/petterreinholdtsen/kodi-invidious-plugin + + resources/icon.png + + + diff --git a/plugin.video.invidious/resources/icon.png b/plugin.video.invidious/resources/icon.png new file mode 100644 index 000000000..9e7fc0e5a Binary files /dev/null and b/plugin.video.invidious/resources/icon.png differ diff --git a/plugin.video.invidious/resources/language/Makefile b/plugin.video.invidious/resources/language/Makefile new file mode 100644 index 000000000..bdfbfc134 --- /dev/null +++ b/plugin.video.invidious/resources/language/Makefile @@ -0,0 +1,4 @@ +# Update translations when the english master changes + +update-po: + msgmerge -U --no-wrap resource.language.nb_no/strings.po resource.language.en_gb/strings.po diff --git a/plugin.video.invidious/resources/language/README b/plugin.video.invidious/resources/language/README new file mode 100644 index 000000000..d184d9ed2 --- /dev/null +++ b/plugin.video.invidious/resources/language/README @@ -0,0 +1,6 @@ +To add a new language, create the language folder +(I.e. resource.language.de_de/, resource.language.da_dk/, resource.language.fr_fr/ etc.) +Run "msginit -i resource.language.en_gb/strings.po". +This will procduce an XX.po file, where XX is the current configured Locale language of your system. +Move that file into your language folder, and name the file strings.po. +For french it will be "mv fr.po resource.language.fr_fr/strings.po" diff --git a/plugin.video.invidious/resources/language/resource.language.en_gb/strings.po b/plugin.video.invidious/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 000000000..868c98dbc --- /dev/null +++ b/plugin.video.invidious/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,57 @@ +# Kodi Media Center language file +# Addon Name: YouTube +# Addon id: plugin.video.invidious +# Addon Provider: TheAssassin +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2015-09-21 11:01+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#30000" +msgid "No description available" +msgstr "" + +msgctxt "#30001" +msgid "Search" +msgstr "" + +msgctxt "#30002" +msgid "New search" +msgstr "" + +msgctxt "#30003" +msgid "HTTP error" +msgstr "" + +msgctxt "#30004" +msgid "Request to Invidious API failed: HTTP status " +msgstr "" + +msgctxt "#30005" +msgid "Timeout" +msgstr "" + +msgctxt "#30006" +msgid "Request to Invidious API exceeded timeout" +msgstr "" + +msgctxt "#30007" +msgid "Instance URL" +msgstr "" + +msgctxt "#30008" +msgid "Instance settings" +msgstr "" + +msgctxt "#30009" +msgid "Disable Dynamic Adaptive Streaming over HTTP (MPEG-DASH)" +msgstr "" diff --git a/plugin.video.invidious/resources/language/resource.language.nb_no/strings.po b/plugin.video.invidious/resources/language/resource.language.nb_no/strings.po new file mode 100644 index 000000000..bc9b30265 --- /dev/null +++ b/plugin.video.invidious/resources/language/resource.language.nb_no/strings.po @@ -0,0 +1,60 @@ +# Kodi Media Center language file +# Addon Name: YouTube +# Addon id: plugin.video.invidious +# Addon Provider: TheAssassin +# Johnny A. Solbu, 2023 +# Petter Reinholdtsen, 2023 +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2015-09-21 11:01+0000\n" +"PO-Revision-Date: 2023-08-07 11:19+0200\n" +"Last-Translator: Petter Reinholdtsen \n" +"Language-Team: Norwegian Bokmal \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.4.2\n" + +msgctxt "#30000" +msgid "No description available" +msgstr "Ingen beskrivelse tilgjengelig" + +msgctxt "#30001" +msgid "Search" +msgstr "Søk" + +msgctxt "#30002" +msgid "New search" +msgstr "Nytt søk" + +msgctxt "#30003" +msgid "HTTP error" +msgstr "HTTP-feil" + +msgctxt "#30004" +msgid "Request to Invidious API failed: HTTP status " +msgstr "Forespørsel til Invidious-API mislyktes: HTTP-status " + +msgctxt "#30005" +msgid "Timeout" +msgstr "Tidsavbrudd" + +msgctxt "#30006" +msgid "Request to Invidious API exceeded timeout" +msgstr "Forespørselen til Invidious-API har overskredet tidsavbruddet" + +msgctxt "#30007" +msgid "Instance URL" +msgstr "Instans-URL" + +msgctxt "#30008" +msgid "Instance settings" +msgstr "Instans-innstillinger" + +msgctxt "#30009" +msgid "Disable Dynamic Adaptive Streaming over HTTP (MPEG-DASH)" +msgstr "Koble ut dynamisk adaptiv strømming over HTTP (MPEG-DASH)" diff --git a/plugin.video.invidious/resources/lib/invidious_addon.py b/plugin.video.invidious/resources/lib/invidious_addon.py new file mode 100644 index 000000000..c73812006 --- /dev/null +++ b/plugin.video.invidious/resources/lib/invidious_addon.py @@ -0,0 +1,18 @@ +import sys + +import invidious_plugin + +import xbmc +import xbmcplugin + + +def main(): + plugin = invidious_plugin.InvidiousPlugin.from_argv() + + xbmcplugin.setContent(plugin.addon_handle, "videos") + + return plugin.run() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugin.video.invidious/resources/lib/invidious_api.py b/plugin.video.invidious/resources/lib/invidious_api.py new file mode 100644 index 000000000..6ee905198 --- /dev/null +++ b/plugin.video.invidious/resources/lib/invidious_api.py @@ -0,0 +1,179 @@ +import time +from collections import namedtuple + +import requests +import xbmc +import xbmcaddon +import xbmcgui + +VideoListItem = namedtuple("VideoSearchResult", + [ + "type", + "id", + "thumbnail_url", + "heading", + "author", + "description", + "view_count", + "published", + "duration", + ] +) + +ChannelListItem = namedtuple("ChannelSearchResult", + [ + "type", + "id", + "thumbnail_url", + "heading", + "description", + "verified", + "sub_count", + ] +) + +PlaylistListItem = namedtuple("PlaylistSearchResult", + [ + "type", + "id", + "thumbnail_url", + "heading", + "channel", + "channel_id", + "verified", + "video_count", + ] +) + + +class InvidiousAPIClient: + def __init__(self, instance_url): + self.instance_url = instance_url.rstrip("/") + self.addon = xbmcaddon.Addon() + + def make_get_request(self, *path, **params): + base_url = self.instance_url + "/api/v1/" + + url_path = "/".join(path) + + while "//" in url_path: + url_path = url_path.replace("//", "/") + + assembled_url = base_url + url_path + + xbmc.log(f"invidious ========== request {assembled_url} with {params} started ==========", xbmc.LOGDEBUG) + start = time.time() + response = requests.get(assembled_url, params=params, timeout=5) + end = time.time() + xbmc.log(f"invidious ========== request finished in {end - start}s ==========", xbmc.LOGDEBUG) + + if response.status_code > 300: + xbmc.log(f'invidious API request {assembled_url} with {params} failed with HTTP status {response.status_code}: {response.reason}.', xbmc.LOGWARNING) + dialog = xbmcgui.Dialog() + dialog.notification( + 'API request failed', + f'HTTP request {assembled_url} with {params} returned HTTP status {response.status_code}: {response.reason} error' + ) + return None + + return response + + def parse_response(self, response): + if not response: + return + data = response.json() + + # If a channel or playlist is opened, the videos are packaged + # in a dict entry "videos". + if "videos" in data: + data = data["videos"] + + for item in data: + # Playlist videos do not have the 'type' attribute + if not "type" in item or item["type"] in ["video", "shortVideo"]: + # Skip videos with no or negative duration. + if not item["lengthSeconds"] > 0: + continue + for thumb in item["videoThumbnails"]: + + # high appears to be ~480x360, which is a + # reasonable trade-off works well on 1080p. + if thumb["quality"] == "high": + thumbnail_url = thumb["url"] + break + + # as a fallback, we just use the last one in the list + # (which is usually the lowest quality). + else: + thumbnail_url = item["videoThumbnails"][-1]["url"] + + yield VideoListItem( + "video", + item["videoId"], + thumbnail_url, + item["title"], + item["author"], + item.get("description", self.addon.getLocalizedString(30000)), + item.get("viewCount", -1), # Missing for playlists. + item.get("published", 0), # Missing for playlists. + item["lengthSeconds"], + ) + elif item["type"] == "channel": + # Grab the highest resolution avatar image + # Usually isn't more than 512x512 + thumbnail = sorted(item["authorThumbnails"], key=lambda thumb: thumb["height"], reverse=True)[0] + + yield ChannelListItem( + "channel", + item["authorId"], + "https:" + thumbnail["url"], + item["author"], + item["description"], + item["authorVerified"], + item["subCount"], + ) + elif item["type"] == 'playlist': + yield PlaylistListItem( + "playlist", + item["playlistId"], + item["playlistThumbnail"], + item["title"], + item["author"], + item["authorId"], + item["authorVerified"], + item["videoCount"], + ) + else: + xbmc.log(f'invidious received search result item with unknown response type {item["type"]}.', xbmc.LOGWARNING) + + def search(self, *terms): + params = { + "q": " ".join(terms), + "sort_by": "upload_date", + } + + response = self.make_get_request("search", **params) + + return self.parse_response(response) + + def fetch_video_information(self, video_id): + response = self.make_get_request("videos/", video_id) + + data = response.json() + + return data + + def fetch_channel_list(self, channel_id): + response = self.make_get_request(f"channels/videos/{channel_id}") + + return self.parse_response(response) + + def fetch_playlist_list(self, playlist_id): + response = self.make_get_request(f"playlists/{playlist_id}") + + return self.parse_response(response) + + def fetch_special_list(self, special_list_name): + response = self.make_get_request(special_list_name) + + return self.parse_response(response) diff --git a/plugin.video.invidious/resources/lib/invidious_plugin.py b/plugin.video.invidious/resources/lib/invidious_plugin.py new file mode 100644 index 000000000..5668072e4 --- /dev/null +++ b/plugin.video.invidious/resources/lib/invidious_plugin.py @@ -0,0 +1,338 @@ +from datetime import datetime + +import json +import requests +import sys +from urllib.parse import urlencode +from urllib.parse import parse_qs + +import requests +import xbmc +import xbmcgui +import xbmcaddon +import xbmcplugin +import xbmcvfs + +import inputstreamhelper + +import invidious_api + +class SearchHistory(): + """Keep fixed length list of search queries, with the latest search + query top.""" + + def __init__(self, history_path, depth=10): + self.history_path = history_path + self.depth = depth + + + def push(self, query): + if xbmcvfs.exists(self.history_path): + with open(self.history_path, "r") as file: + queries = json.load(file) + else: + queries = [] + + if query in queries: + # Remove existing entry to move it forward + queries.remove(query) + + queries.insert(0, query) + + queries = queries[:self.depth] + + with open(self.history_path, "w+") as file: + json.dump(queries, file) + + + def queries(self): + if not xbmcvfs.exists(self.history_path): + return [] + with open(self.history_path, "r") as file: + return json.load(file) + + +class InvidiousPlugin: + # special lists provided by the Invidious API + SPECIAL_LISTS = ("trending", "popular") + + INSTANCESURL = "https://api.invidious.io/instances.json?sort_by=type,health" + def __init__(self, base_url, addon_handle, args): + self.base_url = base_url + self.addon_handle = addon_handle + self.addon = xbmcaddon.Addon() + self.args = args + path = xbmcvfs.translatePath(self.addon.getAddonInfo('profile')) + self.search_history = SearchHistory(path + 'search-history.json', 20) + + instance_url = xbmcplugin.getSetting(self.addon_handle, "instance_url") + if 'auto' == instance_url: + instance_url = self.instance_autodetect() + xbmc.log(f'invidous using instance {instance_url}.', xbmc.LOGINFO) + self.api_client = invidious_api.InvidiousAPIClient(instance_url) + self.disable_dash = \ + ("true" == xbmcplugin.getSetting(self.addon_handle, + "disable_dash")) + def instance_autodetect(self): + xbmc.log('invidious picking instance automatically.', xbmc.LOGINFO) + + response = requests.get(self.INSTANCESURL, timeout=5) + data = response.json() + for instanceinfo in data: + xbmc.log('invidious considering instance ' + str(instanceinfo), xbmc.LOGDEBUG) + instancename, instance = instanceinfo + if 'https' == instance['type']: + instance_url = instance['uri'] + # Make sure the instance work for us. This test avoid + # those rejecting us with HTTP status 429. + api_client = invidious_api.InvidiousAPIClient(instance_url) + if api_client.fetch_special_list(self.SPECIAL_LISTS[0]): + return instance_url + + xbmc.log('invidious no working https type instance returned from api.invidious.io.', xbmc.LOGWARNING) + # FIXME figure out how to show failing autodetection to the user. + dialog = xbmcgui.Dialog() + dialog.notification( + 'No working instance URL found', + 'No working https type instance returned from api.invidious.io.' + "error" + ) + raise ValueError("unable to find working Invidious instance") + + def build_url(self, action, **kwargs): + if not action: + raise ValueError("you need to specify an action") + + kwargs["action"] = action + + return self.base_url + "?" + urlencode(kwargs) + + def add_directory_item(self, *args, **kwargs): + xbmcplugin.addDirectoryItem(self.addon_handle, *args, **kwargs) + + def end_of_directory(self): + xbmcplugin.endOfDirectory(self.addon_handle) + + def display_search_results(self, results): + # FIXME Add pagination support? + for result in results: + if result.type not in ['video', 'channel', 'playlist']: + raise RuntimeError("unknown result type " + result.type) + + list_item = xbmcgui.ListItem(result.heading) + list_item.setArt({ + "thumb": result.thumbnail_url, + }) + + # if this is NOT set, the plugin is called with an invalid handle when trying to play this item + # seriously, Kodi? come on... + # https://forum.kodi.tv/showthread.php?tid=173986&pid=1519987#pid1519987 + list_item.setProperty("IsPlayable", "true") + + if 'video' == result.type: + datestr = datetime.utcfromtimestamp(result.published).date().isoformat() + + list_item.setInfo("video", { + "title": result.heading, + "mediatype": "video", + "plot": result.description, + "credits": result.author, + "date": datestr, + "dateadded": datestr, + "duration": result.duration + }) + + url = self.build_url("play_video", video_id=result.id) + self.add_directory_item(url=url, listitem=list_item) + elif 'channel' == result.type: + url = self.build_url("view_channel", channel_id=result.id) + self.add_directory_item(url=url, listitem=list_item, isFolder=True) + elif 'playlist' == result.type: + url = self.build_url("view_playlist", playlist_id=result.id) + self.add_directory_item(url=url, listitem=list_item, isFolder=True) + + self.end_of_directory() + + def display_new_search(self): + # query search with a dialog + dialog = xbmcgui.Dialog() + search_input = dialog.input(self.addon.getLocalizedString(30001), type=xbmcgui.INPUT_ALPHANUM) + + self.display_search_result(search_input) + + def display_search_result(self, search_input): + if len(search_input) == 0: + return + + self.search_history.push(search_input) + + xbmc.log(f"invidious searching for {search_input}.", xbmc.LOGDEBUG) + + # pass search query to Invidious + results = self.api_client.search(search_input) + + # assemble menu with the results + self.display_search_results(results) + + def display_special_list(self, special_list_name): + if special_list_name not in self.__class__.SPECIAL_LISTS: + raise ValueError(str(special_list_name) + " is not a valid special list") + + videos = self.api_client.fetch_special_list(special_list_name) + + self.display_search_results(videos) + + def display_channel_list(self, channel_id): + videos = self.api_client.fetch_channel_list(channel_id) + + self.display_search_results(videos) + + def display_playlist_list(self, playlist_id): + videos = self.api_client.fetch_playlist_list(playlist_id) + + self.display_search_results(videos) + + def play_video(self, id): + # TODO: add support for adaptive streaming + video_info = self.api_client.fetch_video_information(id) + + xbmc.log(f"invidious playing video {video_info}.", xbmc.LOGDEBUG) + + listitem = None + # check if playback via MPEG-DASH is possible + if not self.disable_dash and "dashUrl" in video_info: + is_helper = inputstreamhelper.Helper("mpd") + + if is_helper.check_inputstream(): + url = video_info["dashUrl"] + xbmc.log(f"invidious using mpeg-dash stream {url}.", xbmc.LOGDEBUG) + listitem = xbmcgui.ListItem(path=url) + listitem.setProperty("inputstream", is_helper.inputstream_addon) + listitem.setProperty("inputstream.adaptive.manifest_type", "mpd") + else: + xbmc.log("invidious mpeg-dash input helper not available.", xbmc.LOGDEBUG) + + # as a fallback, we use the last oldschool stream, as it is + # often the best quality. + if listitem is None: + url = video_info["formatStreams"][-1]["url"] + xbmc.log(f"invidious playback failing back to non-dash stream {url}!", xbmc.LOGINFO) + # it's pretty complicated to play a video by its URL in Kodi... + listitem = xbmcgui.ListItem(path=url) + + datestr = datetime.utcfromtimestamp(video_info["published"]).date().isoformat() + listitem.setInfo('video', { + "title": video_info["title"], + "mediatype": "video", + "plot": video_info["description"], + "credits": video_info["author"], + "date": datestr, + "dateadded": datestr, + "duration": str(video_info["lengthSeconds"]) + }) + xbmcplugin.setResolvedUrl(self.addon_handle, succeeded=True, listitem=listitem) + + def display_main_menu(self): + def add_list_item(label, path): + listitem = xbmcgui.ListItem(label, path=path, ) + self.add_directory_item(url=self.build_url(path), listitem=listitem, isFolder=True) + + # video search item + add_list_item(self.addon.getLocalizedString(30001), "search_menu") + + for special_list_name in self.__class__.SPECIAL_LISTS: + label = special_list_name[0].upper() + special_list_name[1:] + add_list_item(label, special_list_name) + + self.end_of_directory() + + def display_search_submenu(self): + def add_list_item(label, path): + listitem = xbmcgui.ListItem(label, path=path, ) + self.add_directory_item(url=self.build_url(path), listitem=listitem, isFolder=True) + + # New search on top. + add_list_item(self.addon.getLocalizedString(30002), "new_search") + + for query in self.search_history.queries(): + url = self.build_url("search", q=query) + listitem = xbmcgui.ListItem(query, path=query, ) + self.add_directory_item( + url=url, + listitem=listitem, + isFolder=True + ) + + self.end_of_directory() + + def run(self): + """ + Web application style method dispatching. + Uses querystring only, which is pretty oldschool CGI-like stuff. + """ + + action = self.args.get("action", [None])[0] + + # debugging + xbmc.log("invidous --------------------------------------------", xbmc.LOGDEBUG) + xbmc.log("invidous base url:" + str(self.base_url), xbmc.LOGDEBUG) + xbmc.log("invidous handle:" + str(self.addon_handle), xbmc.LOGDEBUG) + xbmc.log("invidous args:" + str(self.args), xbmc.LOGDEBUG) + xbmc.log("invidous action:" + str(action), xbmc.LOGDEBUG) + xbmc.log("invidous --------------------------------------------", xbmc.LOGDEBUG) + + # for the sake of simplicity, we just handle HTTP request errors here centrally + try: + if not action: + self.display_main_menu() + + elif action == "search_menu": + self.display_search_submenu() + + elif action == "new_search": + self.display_new_search() + + elif action == "search": + self.display_search_result(self.args["q"][0]) + + elif action == "play_video": + self.play_video(self.args["video_id"][0]) + + elif action == "view_channel": + self.display_channel_list(self.args["channel_id"][0]) + + elif action == "view_playlist": + self.display_playlist_list(self.args["playlist_id"][0]) + + elif action in self.__class__.SPECIAL_LISTS: + special_list_name = action + self.display_special_list(special_list_name) + + else: + raise RuntimeError("unknown action " + action) + + except requests.HTTPError as e: + xbmc.log(f'invidous HTTP status {e.response.status_code} during action processing: {e.response.reason}', xbmc.LOGWARNING) + dialog = xbmcgui.Dialog() + dialog.notification( + self.addon.getLocalizedString(30003), + self.addon.getLocalizedString(30004) + str(e.response.status_code), + "error" + ) + + except requests.Timeout: + xbmc.log('invidous HTTP timed out during action processing', xbmc.LOGWARNING) + dialog = xbmcgui.Dialog() + dialog.notification( + self.addon.getLocalizedString(30005), + self.addon.getLocalizedString(30006), + "error" + ) + + @classmethod + def from_argv(cls): + base_url = sys.argv[0] + addon_handle = int(sys.argv[1]) + args = parse_qs(sys.argv[2][1:]) + + return cls(base_url, addon_handle, args) diff --git a/plugin.video.invidious/resources/settings.xml b/plugin.video.invidious/resources/settings.xml new file mode 100644 index 000000000..f8af53d49 --- /dev/null +++ b/plugin.video.invidious/resources/settings.xml @@ -0,0 +1,7 @@ + + + + + + +