From 8e444c40ed22b10977d676aaa9109f1ac565bd9d Mon Sep 17 00:00:00 2001 From: kevinSatizabal Date: Sun, 14 Jul 2024 18:04:20 -0500 Subject: [PATCH 1/9] Fix section Programs --- addon.xml | 7 +++--- resources/lib/plugin.py | 12 +++++----- resources/lib/search.py | 49 +++++++++++++++++++++++------------------ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/addon.xml b/addon.xml index 5570fd1..9ddff9f 100644 --- a/addon.xml +++ b/addon.xml @@ -1,11 +1,11 @@ - + - + - + video @@ -22,6 +22,7 @@ Watch live TV and a range of programmes available via the DW website fraser.chapman@gmail.com https://github.com/FraserChapman/plugin.video.dw v1.0.0 (14-7-19) - Initial version + v1.0.1 (14-7-19) - Update kodi to version 19 or newer Neither this addon nor its author are in anyway affiliated with Deutsche Welle resources/icon.png diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 1800dec..8743dbc 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -147,14 +147,14 @@ def programme(): if not href: # TV Shows menu soup = dws.get_html(dws.DW_PROGRAMME_URI) - content = soup.find("div", {"id": "bodyContent"}).extract() - items = content.find_all("div", "epg") + content = soup.find("section", {"id": "program-video-list"}).extract() + items = content.find_all("div", {"class": "teaser-wrap"}) for item in items: - img = item.find("img") - title = item.find("h2").text.encode("utf-8") - action = item.find("a", string="All videos") + img = item.find("img", {"class", "hq-img"}) + title = item.find("h3").text.encode("utf-8") + action = item.find("a", {"class": "teaser-image"}) pid = dws.get_program_id(action.get("href")) - plot = item.find("p").text.strip() + plot = item.find("div", {"class","teaser-description"}).text.strip() add_menu_item(programme, title, {"href": dws.get_search_url(pid=pid), "category": title}, diff --git a/resources/lib/search.py b/resources/lib/search.py index 24ba89c..b9b894b 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -13,11 +13,13 @@ from . import kodiutils as ku +import xbmc + DW_URI = "https://www.dw.com/" DW_MEDIA_URL = "{}en/media-center/".format(DW_URI) DW_MEDIA_LIVE_URI = "{}live-tv/s-100825".format(DW_MEDIA_URL) DW_MEDIA_ALL_URL = "{}all-media-content/s-100826".format(DW_MEDIA_URL) -DW_PROGRAMME_URI = "{}en/tv/tv-programs/s-9103".format(DW_URI) + DW_SEARCH_TEMPLATE = "{}mediafilter/research?" \ "lang={{}}&type=18&results=0&showteasers=t&first={{}}{{}}{{}}{{}}".format(DW_URI) @@ -28,37 +30,41 @@ SEARCH_MAX_RESULTS = ku.get_setting_as_int("search_max_results") SEARCH_TIMEOUT = 60 +def get_programme_uri(): + """Retrieve the program URL for a specific language.""" + if SEARCH_LANGUAGE == 'en': + return "{}en/all-shows/programs-en".format(DW_URI) + elif SEARCH_LANGUAGE == 'es': + return "{}es/todos-los-programas/programs-es".format(DW_URI) + elif SEARCH_LANGUAGE == 'ar': + return "{}ar/جميع-البرامج/programs-ar".format(DW_URI) + elif SEARCH_LANGUAGE == 'de': + return "{}de/alle-sendungen/programs-de".format(DW_URI) + +DW_PROGRAMME_URI = get_programme_uri() + searches = Store("app://saved-searches") recents = Store("app://recently-viewed") logger = logging.getLogger(__name__) - def get_info(href): # type: (str) -> dict """Gets the info for playable item; title, image, path, etc""" soup = get_html(href) - item = soup.find("div", "mediaItem").extract() - plot = soup.find("p", "intro") - title = item.find("input", {"name": "media_title"}).get("value") - video = soup.find("meta", {"property": "og:video"}) - file_name = item.find("input", {"name": "file_name"}) - duration = item.find("input", {"name": "file_duration"}) - preview_image = item.find("input", {"name": "preview_image"}) - path = None - if video: - path = video.get("content") - elif file_name: - flv = file_name.get("value") - path = get_m3u8_url(flv) - if not path: # fallback to the low-res flv if no mp4 or mu38... - path = flv + item = soup.find("article", {"class": "sk6xmai"}).extract() + plot = soup.find("p", {"class": "teaser-text"}) + title = item.find("h1", {"class": "headline"}).get("value") + video = item.find("video", {"class": "dw-player"}) + duration = video.get('data-duration') + preview_image = video.get('poster') + path = video.find('source').get('src') return { "path": path, - "image": get_url(preview_image.get("value")) if preview_image else "", + "image": get_url(str(preview_image)) if preview_image else "", "info": { "title": title, "plot": plot.text if plot else title, - "duration": int(duration.get("value")) if duration else 0 + "duration": str(duration) if duration else 0 } } @@ -66,7 +72,7 @@ def get_info(href): def get_program_id(url): # type: (str) -> str """Attempts to extract a programme id from a given url""" - search = re.search(r"programs=(\d+)", url) + search = re.search(r"program-(\d+)", url) return search.group(1) if search else "" @@ -103,6 +109,7 @@ def get_url(href): def get_m3u8_url(flv): # type: (str) -> Optional[str] """Attempts to generate a m3u8 URL from the given flv URL""" + xbmc.log(str(flv)) try: return DW_VIDEO_TEMPLATE.format(flv.split("flv/")[1].split("vp6")[0]) except IndexError: @@ -111,7 +118,7 @@ def get_m3u8_url(flv): def get_search_url(query=None, tid=None, pid=None): """Gets a full URL for a search page""" - language = "en" if tid or pid else SEARCH_LANGUAGE + language = SEARCH_LANGUAGE query = "" if query is None else "&filter={}".format(query) tid = "" if tid is None else "&themes={}".format(tid) pid = "" if pid is None else "&programs={}".format(pid) From 4d9831258766e3f8146512e2331cce261c5d04e6 Mon Sep 17 00:00:00 2001 From: kevinSatizabal Date: Sat, 20 Jul 2024 14:22:38 -0500 Subject: [PATCH 2/9] fix live TV --- resources/lib/plugin.py | 50 +++++++++++++++++++++++++++-------------- resources/lib/search.py | 27 ++++++---------------- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 8743dbc..1934021 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -10,6 +10,8 @@ import xbmc import xbmcaddon import xbmcplugin +import json +import requests from xbmcgui import ListItem from resources.lib import kodilogging @@ -120,23 +122,37 @@ def index(): @plugin.route("/live") def live(): - soup = dws.get_html(dws.DW_MEDIA_LIVE_URI) - items = soup.find_all("div", "mediaItem") - for item in items: - title = item.find("input", {"name": "media_title"}).get("value").encode("utf-8") - preview_image = dws.get_url(item.find("input", {"name": "preview_image"}).get("value")) - add_menu_item(play_film, - item.find("input", {"name": "channel_name"}).get("value"), - { - "m3u8": item.find("input", {"name": "file_name"}).get("value"), - "title": title - }, - ku.art(preview_image), - {"plot": title}, - False) - xbmcplugin.setContent(plugin.handle, "tvshows") - xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32006)) # Live TV - xbmcplugin.endOfDirectory(plugin.handle) + if dws.SEARCH_LANGUAGE == 'en' or dws.SEARCH_LANGUAGE == 'de': + lang = 'english' + elif dws.SEARCH_LANGUAGE == 'es': + lang = 'spanish' + elif dws.SEARCH_LANGUAGE == 'ar': + lang = 'arabic' + else: + lang = '' + + response = requests.get(dws.DW_MEDIA_LIVE_URI.format(lang)) + if response.status_code == 200: + items = response.json() + + for item in items["data"]["livestreamChannels"]: + title = item["nextTimeSlots"][0]["program"]["name"] + preview_image = item["nextTimeSlots"][0]["program"]["posterImageUrl"] + add_menu_item(play_film, + item["name"], + { + "m3u8": item["hlsVideoSrc"], + "title": title + }, + preview_image, + {"plot": title}, + False) + xbmcplugin.setContent(plugin.handle, "tvshows") + xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32006)) # Live TV + xbmcplugin.endOfDirectory(plugin.handle) + else: + xbmc.log("Error in the request for live TV: " + str(dws.DW_MEDIA_LIVE_URI.format(lang)), level=xbmc.LOGERROR) + @plugin.route("/programme") diff --git a/resources/lib/search.py b/resources/lib/search.py index b9b894b..fe1f205 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -15,34 +15,21 @@ import xbmc +SEARCH_SAVED = ku.get_setting_as_bool("search_saved") +SEARCH_LANGUAGE = ku.get_setting("search_language") +SEARCH_MAX_RESULTS = ku.get_setting_as_int("search_max_results") +SEARCH_TIMEOUT = 60 + DW_URI = "https://www.dw.com/" DW_MEDIA_URL = "{}en/media-center/".format(DW_URI) -DW_MEDIA_LIVE_URI = "{}live-tv/s-100825".format(DW_MEDIA_URL) +DW_MEDIA_LIVE_URI = "{}graph-api/en/livestream/{{}}".format(DW_URI) DW_MEDIA_ALL_URL = "{}all-media-content/s-100826".format(DW_MEDIA_URL) - +DW_PROGRAMME_URI = "{}en/all-shows/programs-{}".format(DW_URI, SEARCH_LANGUAGE) DW_SEARCH_TEMPLATE = "{}mediafilter/research?" \ "lang={{}}&type=18&results=0&showteasers=t&first={{}}{{}}{{}}{{}}".format(DW_URI) DW_VIDEO_TEMPLATE = "https://dwhlsondemand-vh.akamaihd.net/i/dwtv_video/flv/{},sor,avc,.mp4.csmil/master.m3u8" -SEARCH_SAVED = ku.get_setting_as_bool("search_saved") -SEARCH_LANGUAGE = ku.get_setting("search_language") -SEARCH_MAX_RESULTS = ku.get_setting_as_int("search_max_results") -SEARCH_TIMEOUT = 60 - -def get_programme_uri(): - """Retrieve the program URL for a specific language.""" - if SEARCH_LANGUAGE == 'en': - return "{}en/all-shows/programs-en".format(DW_URI) - elif SEARCH_LANGUAGE == 'es': - return "{}es/todos-los-programas/programs-es".format(DW_URI) - elif SEARCH_LANGUAGE == 'ar': - return "{}ar/جميع-البرامج/programs-ar".format(DW_URI) - elif SEARCH_LANGUAGE == 'de': - return "{}de/alle-sendungen/programs-de".format(DW_URI) - -DW_PROGRAMME_URI = get_programme_uri() - searches = Store("app://saved-searches") recents = Store("app://recently-viewed") logger = logging.getLogger(__name__) From 853dd573901c95ebf2cfe02a2b437c7b17c6c63f Mon Sep 17 00:00:00 2001 From: kevinSatizabal Date: Wed, 31 Jul 2024 21:34:54 -0500 Subject: [PATCH 3/9] Fixed the section for programs in languages other than English --- resources/lib/plugin.py | 1 - resources/lib/search.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 1934021..6afb451 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -10,7 +10,6 @@ import xbmc import xbmcaddon import xbmcplugin -import json import requests from xbmcgui import ListItem diff --git a/resources/lib/search.py b/resources/lib/search.py index fe1f205..a328833 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -151,7 +151,7 @@ def get_html(url): r = requests.get(url, headers=headers, timeout=SEARCH_TIMEOUT) if 200 == r.status_code: soup = BeautifulSoup(r.content, "html.parser") - c.set(url, r.content, r.headers) + # c.set(url, r.content, r.headers) return soup if 304 == r.status_code: c.touch(url, r.headers) From 19e331405e82d21e590475a1224b81538852a4e5 Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Mon, 9 Feb 2026 21:46:54 -0500 Subject: [PATCH 4/9] Nuevo desarrollo basado en el api de android TV --- addon.py | 3 + addon.xml | 10 +- main.py | 5 - resources/lib/endpoints.py | 4 + resources/lib/kodilogging.py | 42 ----- resources/lib/kodiutils.py | 92 ---------- resources/lib/main.py | 36 ++++ resources/lib/plugin.py | 324 ----------------------------------- resources/lib/search.py | 160 ----------------- 9 files changed, 47 insertions(+), 629 deletions(-) create mode 100644 addon.py delete mode 100644 main.py create mode 100644 resources/lib/endpoints.py delete mode 100644 resources/lib/kodilogging.py delete mode 100644 resources/lib/kodiutils.py create mode 100644 resources/lib/main.py delete mode 100644 resources/lib/plugin.py delete mode 100644 resources/lib/search.py diff --git a/addon.py b/addon.py new file mode 100644 index 0000000..be08638 --- /dev/null +++ b/addon.py @@ -0,0 +1,3 @@ +from resources.lib import main +if __name__ == "__main__": + main.run() \ No newline at end of file diff --git a/addon.xml b/addon.xml index 9ddff9f..2d21590 100644 --- a/addon.xml +++ b/addon.xml @@ -1,13 +1,11 @@ - + - - - - + + - + video diff --git a/main.py b/main.py deleted file mode 100644 index 7c35ad7..0000000 --- a/main.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- - -from resources.lib import plugin - -plugin.run() diff --git a/resources/lib/endpoints.py b/resources/lib/endpoints.py new file mode 100644 index 0000000..3790719 --- /dev/null +++ b/resources/lib/endpoints.py @@ -0,0 +1,4 @@ +class Endpoints: + BASE_URL = "https://api.dw.com/api" + NAVIGATION = f"{BASE_URL}/navigation/{{language}}?product=smarttv&platform=androidtv" + VIDEO_DETAILS = f"{BASE_URL}/detail/video/{{video_id}}" \ No newline at end of file diff --git a/resources/lib/kodilogging.py b/resources/lib/kodilogging.py deleted file mode 100644 index bd47221..0000000 --- a/resources/lib/kodilogging.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -import logging - -import xbmc -import xbmcaddon - -from resources.lib.kodiutils import get_setting_as_bool - - -class KodiLogHandler(logging.StreamHandler): - - def __init__(self): - logging.StreamHandler.__init__(self) - addon_id = xbmcaddon.Addon().getAddonInfo("id") - formatter = logging.Formatter("[{}] %(name)s %(message)s".format(addon_id)) - self.setFormatter(formatter) - - def emit(self, record): - levels = { - logging.CRITICAL: xbmc.LOGFATAL, - logging.ERROR: xbmc.LOGERROR, - logging.WARNING: xbmc.LOGWARNING, - logging.INFO: xbmc.LOGINFO, - logging.DEBUG: xbmc.LOGDEBUG, - logging.NOTSET: xbmc.LOGNONE, - } - if get_setting_as_bool("debug"): - try: - xbmc.log(self.format(record), levels[record.levelno]) - except UnicodeEncodeError: - xbmc.log(self.format(record).encode( - "utf-8", "ignore"), levels[record.levelno]) - - def flush(self): - pass - - -def config(): - logger = logging.getLogger() - logger.addHandler(KodiLogHandler()) - logger.setLevel(logging.DEBUG) diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py deleted file mode 100644 index decee5f..0000000 --- a/resources/lib/kodiutils.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Kodi gui and settings helpers""" - -__author__ = "fraser" - -import os - -import xbmc -import xbmcaddon -import xbmcgui - -ADDON = xbmcaddon.Addon() -ADDON_NAME = ADDON.getAddonInfo("name") -ADDON_PATH = ADDON.getAddonInfo("path") -MEDIA_URI = os.path.join(ADDON_PATH, "resources", "media") - - -def art(image): - # type: (str) -> dict - return { - "icon": image, - "thumb": image, - "fanart": image, - "poster": image - } - - -def icon(image): - # type: (str) -> dict - """Creates the application folder icon info for main menu items""" - return {"icon": os.path.join(MEDIA_URI, image)} - - -def user_input(): - # type: () -> Union[str, bool] - keyboard = xbmc.Keyboard("", "{} {}".format(localize(32007), ADDON_NAME)) # search - keyboard.doModal() - if keyboard.isConfirmed(): - return keyboard.getText() - return False - - -def confirm(): - # type: () -> bool - return xbmcgui.Dialog().yesno(ADDON_NAME, localize(32022)) # Are you sure? - - -def notification(header, message, time=5000, image=ADDON.getAddonInfo("icon"), sound=True): - # type: (str, str, int, str, bool) -> None - xbmcgui.Dialog().notification(header, str(message), image, time, sound) - - -def show_settings(): - # type: () -> None - ADDON.openSettings() - - -def get_setting(setting): - # type: (str) -> str - return ADDON.getSetting(setting).strip() - - -def set_setting(setting, value): - # type: (str, Any) -> None - ADDON.setSetting(setting, str(value)) - - -def get_setting_as_bool(setting): - # type: (str) -> bool - return ADDON.getSettingBool(setting) - - -def get_setting_as_float(setting): - # type: (str) -> float - try: - return ADDON.getSettingNumber(setting) - except ValueError: - return 0 - - -def get_setting_as_int(setting): - # type: (str) -> int - try: - return ADDON.getSettingInt(setting) - except ValueError: - return 0 - - -def localize(token): - # type: (int) -> str - return ADDON.getLocalizedString(token).encode("utf-8", "ignore").decode("utf-8") diff --git a/resources/lib/main.py b/resources/lib/main.py new file mode 100644 index 0000000..d1338f5 --- /dev/null +++ b/resources/lib/main.py @@ -0,0 +1,36 @@ +from codequick import Route, Listitem, run, Script, utils +from resources.lib.endpoints import Endpoints +import simplejson as json +import requests + +@Route.register +def root(plugin): + resp = requests.get(Endpoints.NAVIGATION.format(language="es_ES")) + + if resp.status_code == 200: + root_elem = json.loads(resp.text) + + # Parse each category + for elem in root_elem["items"]: + item = Listitem() + + # The image tag contains both the image url and title + # img = elem.find(".//img") + + # Set the thumbnail image + # item.art["thumb"] = img.get("src") + + # Set the title + item.label = elem["name"] + + # Fetch the url + # url = elem.find("div/a").get("href") + + # This will set the callback that will be called when listitem is activated. + # 'video_list' is the route callback function that we will create later. + # The 'url' argument is the url of the category that will be passed + # to the 'video_list' callback. + # item.set_callback(video_list, url=url) + + # Return the listitem as a generator. + yield item \ No newline at end of file diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py deleted file mode 100644 index 6afb451..0000000 --- a/resources/lib/plugin.py +++ /dev/null @@ -1,324 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Main plugin file - Handles the various routes""" - -__author__ = "fraser" - -import logging - -import routing -import xbmc -import xbmcaddon -import xbmcplugin -import requests -from xbmcgui import ListItem - -from resources.lib import kodilogging -from resources.lib import kodiutils as ku -from resources.lib import search as dws - -kodilogging.config() -logger = logging.getLogger(__name__) -plugin = routing.Plugin() -ADDON_NAME = xbmcaddon.Addon().getAddonInfo("name") # Deutsche Welle - - -def parse_search_results(soup, url, method, category): - # type: (BeautifulSoup, str, callable, str) -> None - """Adds menu items for search result data""" - items = soup.find_all("div", "hov") - paginate(soup, url, method, category) - for item in items: - action = item.find("a") - img = action.find("img").get("src") - date, time = dws.get_date_time(action.find("span", "date").text) - plot = action.find("p") - add_menu_item(play_film, - action.find("h2").contents[0], - {"href": dws.get_url(action.get("href"))}, - ku.art(dws.get_url(img)), - { - "plot": plot.text if plot else "", - "date": date.strip(), - "duration": time - }, - False) - xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_DATE) - xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_DURATION) - xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) - - -def paginate(soup, url, method, category): - # type: (BeautifulSoup, str, callable, str) -> None - """Adds pagination to results pages""" - total = int(soup.find("input", {"name": "allResultsAmount"}).extract()["value"]) - hidden = dws.get_hidden(url) - if total > dws.SEARCH_MAX_RESULTS: - offset = hidden + dws.SEARCH_MAX_RESULTS - page = offset // dws.SEARCH_MAX_RESULTS + 1 - add_menu_item(method, - "[{} {}]".format(ku.localize(32011), page), - { - "href": dws.update_hidden(url, offset), - "category": category - }) - - -def add_menu_item(method, label, args=None, art=None, info=None, directory=True): - # type: (Callable, Union[str, int], dict, dict, dict, bool) -> None - """wrapper for xbmcplugin.addDirectoryItem""" - info = {} if info is None else info - art = {} if art is None else art - args = {} if args is None else args - label = ku.localize(label) if isinstance(label, int) else label - list_item = ListItem(label) - list_item.setArt(art) - if method == search and "q" in args: - # saved search menu items can be removed via context menu - list_item.addContextMenuItems([( - ku.localize(32019), - "XBMC.RunPlugin({})".format(plugin.url_for(search, delete=True, q=label)) - )]) - if method in [play_film, programme]: - list_item.setInfo("video", info) - list_item.setProperty("IsPlayable", "true") - xbmcplugin.addDirectoryItem( - plugin.handle, - plugin.url_for(method, **args), - list_item, - directory) - - -def get_arg(key, default=None): - # type: (str, Any) -> Any - """Get the argument value or default""" - if default is None: - default = "" - return plugin.args.get(key, [default])[0] - - -@plugin.route("/") -def index(): - # type: () -> None - """Main menu""" - if ku.get_setting_as_bool("show_live"): - add_menu_item(live, 32006, art=ku.icon("livetv.png")) - if ku.get_setting_as_bool("show_programmes"): - add_menu_item(programme, 32009, art=ku.icon("programme.png")) - if ku.get_setting_as_bool("show_topics"): - add_menu_item(topic, 32008, art=ku.icon("topic.png")) - if ku.get_setting_as_bool("show_past24h"): - add_menu_item(past24h, 32004, art=ku.icon("past24h.png")) - if ku.get_setting_as_bool("show_recent"): - add_menu_item(recent, 32005, art=ku.icon("recent.png")) - if ku.get_setting_as_bool("show_search"): - add_menu_item(search, 32007, {"menu": True}, ku.icon("search.png")) - if ku.get_setting_as_bool("show_settings"): - add_menu_item(settings, 32010, art=ku.icon("settings.png"), directory=False) - xbmcplugin.setPluginCategory(plugin.handle, ADDON_NAME) - xbmcplugin.endOfDirectory(plugin.handle) - - -@plugin.route("/live") -def live(): - if dws.SEARCH_LANGUAGE == 'en' or dws.SEARCH_LANGUAGE == 'de': - lang = 'english' - elif dws.SEARCH_LANGUAGE == 'es': - lang = 'spanish' - elif dws.SEARCH_LANGUAGE == 'ar': - lang = 'arabic' - else: - lang = '' - - response = requests.get(dws.DW_MEDIA_LIVE_URI.format(lang)) - if response.status_code == 200: - items = response.json() - - for item in items["data"]["livestreamChannels"]: - title = item["nextTimeSlots"][0]["program"]["name"] - preview_image = item["nextTimeSlots"][0]["program"]["posterImageUrl"] - add_menu_item(play_film, - item["name"], - { - "m3u8": item["hlsVideoSrc"], - "title": title - }, - preview_image, - {"plot": title}, - False) - xbmcplugin.setContent(plugin.handle, "tvshows") - xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32006)) # Live TV - xbmcplugin.endOfDirectory(plugin.handle) - else: - xbmc.log("Error in the request for live TV: " + str(dws.DW_MEDIA_LIVE_URI.format(lang)), level=xbmc.LOGERROR) - - - -@plugin.route("/programme") -def programme(): - """Shows the programme menu or a programme's playable items""" - href = get_arg("href") - category = get_arg("category", ku.localize(32009)) # Programs - if not href: - # TV Shows menu - soup = dws.get_html(dws.DW_PROGRAMME_URI) - content = soup.find("section", {"id": "program-video-list"}).extract() - items = content.find_all("div", {"class": "teaser-wrap"}) - for item in items: - img = item.find("img", {"class", "hq-img"}) - title = item.find("h3").text.encode("utf-8") - action = item.find("a", {"class": "teaser-image"}) - pid = dws.get_program_id(action.get("href")) - plot = item.find("div", {"class","teaser-description"}).text.strip() - add_menu_item(programme, - title, - {"href": dws.get_search_url(pid=pid), "category": title}, - ku.art(dws.get_url(img.get("src"))), - {"plot": plot if plot else title}) - xbmcplugin.setContent(plugin.handle, "tvshows") - else: - # TV Show's playable episodes - soup = dws.get_html(href) - parse_search_results(soup, href, programme, category) - xbmcplugin.setContent(plugin.handle, "episodes") - xbmcplugin.setPluginCategory(plugin.handle, category) - xbmcplugin.endOfDirectory(plugin.handle) - - -@plugin.route("/topic") -def topic(): - """Shows the topics menu or a topic's playable items""" - href = get_arg("href", False) - category = get_arg("category", ku.localize(32008)) # Themes - if not href: - # Topics menu - soup = dws.get_html(dws.DW_MEDIA_ALL_URL) - content = soup.find("div", {"id": "themes"}).extract() - items = content.find_all("a", "check") - for item in items: - add_menu_item(topic, - item.text, - {"href": dws.get_search_url(tid=item.get("value")), "category": item.text}) - else: - # Topic's playable items - soup = dws.get_html(href) - parse_search_results(soup, href, topic, category) - xbmcplugin.setContent(plugin.handle, "episodes") - xbmcplugin.setPluginCategory(plugin.handle, category) - xbmcplugin.endOfDirectory(plugin.handle) - - -@plugin.route("/past24h") -def past24h(): - """Shows playable items from the last 24 hours""" - url = dws.get_search_url() - soup = dws.get_html(url) - parse_search_results(soup, url, past24h, ku.localize(32004)) - xbmcplugin.setContent(plugin.handle, "episodes") - xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32004)) # Past 24 hours - xbmcplugin.endOfDirectory(plugin.handle) - - -@plugin.route("/recent") -def recent(): - # type: () -> None - """Show recently viewed films""" - items = dws.recents.retrieve() - for url in items: - data = dws.get_info(url) - add_menu_item(play_film, - data.get("info").get("title"), - {"href": url}, - ku.art(data.get("image")), - data.get("info"), - False) - xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32005)) # Recent - xbmcplugin.endOfDirectory(plugin.handle) - - -@plugin.route("/settings") -def settings(): - # type: () -> None - """Addon Settings""" - ku.show_settings() - xbmc.executebuiltin("Container.Refresh()") - - -@plugin.route("/play") -def play_film(): - # type: () -> None - """Show playable item""" - m3u8 = get_arg("m3u8", False) - href = get_arg("href", False) - list_item = ListItem() - if m3u8: - # live tv stream - title = get_arg("title") - list_item.setPath(path=m3u8) - list_item.setInfo("video", {"plot": title}) - elif href: - # other playable item - data = dws.get_info(href) - if not data["path"]: - logger.debug("play_film no path: {}".format(href)) - return - dws.recents.append(href) - list_item.setPath(path=data["path"]) - list_item.setInfo("video", data["info"]) - xbmcplugin.setResolvedUrl(plugin.handle, True, list_item) - - -@plugin.route("/clear/") -def clear(idx): - # type: (str) -> None - """Clear cached or recently played items""" - if idx == "cache" and ku.confirm(): - dws.cache_clear() - elif idx == "recent" and ku.confirm(): - dws.recents.clear() - elif idx == "search" and ku.confirm(): - dws.searches.clear() - - -@plugin.route("/search") -def search(): - # type: () -> Optional[bool] - """Search the archive""" - query = get_arg("q") - href = get_arg("href", False) - category = get_arg("category", ku.localize(32007)) - # Remove saved search item - if bool(get_arg("delete", False)): - dws.searches.remove(query) - xbmc.executebuiltin("Container.Refresh()") - return True - # View saved search menu - elif bool(get_arg("menu", False)): - add_menu_item(search, "[{}]".format(ku.localize(32016)), {"new": True}) # [New Search] - for item in dws.searches.retrieve(): - text = item.encode("utf-8") - add_menu_item(search, text, {"q": text}) - xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32007)) # Search - xbmcplugin.endOfDirectory(plugin.handle) - return True - # New look-up - elif bool(get_arg("new", False)): - query = ku.user_input() - if not query: - return False - category = "{} '{}'".format(ku.localize(32007), query) - if dws.SEARCH_SAVED: - dws.searches.append(query) - # Process search - url = href if href else dws.get_search_url(query=query) - soup = dws.get_html(url) - parse_search_results(soup, url, search, category) - xbmcplugin.setContent(plugin.handle, "videos") - xbmcplugin.setPluginCategory(plugin.handle, category) - xbmcplugin.endOfDirectory(plugin.handle) - - -def run(): - # type: () -> None - """Main entry point""" - plugin.run() diff --git a/resources/lib/search.py b/resources/lib/search.py deleted file mode 100644 index a328833..0000000 --- a/resources/lib/search.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- - -"""BP searcher and helpers""" - -__author__ = "fraser" - -import logging -import re - -import requests -from bs4 import BeautifulSoup -from cache import Cache, Store, conditional_headers - -from . import kodiutils as ku - -import xbmc - -SEARCH_SAVED = ku.get_setting_as_bool("search_saved") -SEARCH_LANGUAGE = ku.get_setting("search_language") -SEARCH_MAX_RESULTS = ku.get_setting_as_int("search_max_results") -SEARCH_TIMEOUT = 60 - -DW_URI = "https://www.dw.com/" -DW_MEDIA_URL = "{}en/media-center/".format(DW_URI) -DW_MEDIA_LIVE_URI = "{}graph-api/en/livestream/{{}}".format(DW_URI) -DW_MEDIA_ALL_URL = "{}all-media-content/s-100826".format(DW_MEDIA_URL) -DW_PROGRAMME_URI = "{}en/all-shows/programs-{}".format(DW_URI, SEARCH_LANGUAGE) - -DW_SEARCH_TEMPLATE = "{}mediafilter/research?" \ - "lang={{}}&type=18&results=0&showteasers=t&first={{}}{{}}{{}}{{}}".format(DW_URI) -DW_VIDEO_TEMPLATE = "https://dwhlsondemand-vh.akamaihd.net/i/dwtv_video/flv/{},sor,avc,.mp4.csmil/master.m3u8" - -searches = Store("app://saved-searches") -recents = Store("app://recently-viewed") -logger = logging.getLogger(__name__) - -def get_info(href): - # type: (str) -> dict - """Gets the info for playable item; title, image, path, etc""" - soup = get_html(href) - item = soup.find("article", {"class": "sk6xmai"}).extract() - plot = soup.find("p", {"class": "teaser-text"}) - title = item.find("h1", {"class": "headline"}).get("value") - video = item.find("video", {"class": "dw-player"}) - duration = video.get('data-duration') - preview_image = video.get('poster') - path = video.find('source').get('src') - return { - "path": path, - "image": get_url(str(preview_image)) if preview_image else "", - "info": { - "title": title, - "plot": plot.text if plot else title, - "duration": str(duration) if duration else 0 - } - } - - -def get_program_id(url): - # type: (str) -> str - """Attempts to extract a programme id from a given url""" - search = re.search(r"program-(\d+)", url) - return search.group(1) if search else "" - - -def time_to_seconds(text): - # type: (str) -> int - """Converts a time in the format mm:ss to seconds, defaults to 0""" - if text == 0: - return 0 - duration = re.search(r"[\d:]+", text) - if duration: - minutes, seconds = duration.group().split(':') - return int(minutes) * 60 + int(seconds) - return 0 - - -def get_date_time(text): - # type: (str) -> tuple - """Attempts to parse date and time from text in the format 'dd.mm.yyyy | mm:ss'""" - try: - date, time = text.split("|") - except ValueError: - date, time = (text, 0) - return date, time_to_seconds(time) - - -def get_url(href): - # type: (str) -> str - """Gets a full URL to a resource""" - return href \ - if href.startswith("http") \ - else "{}{}".format(DW_URI, href.strip().lstrip("/")) - - -def get_m3u8_url(flv): - # type: (str) -> Optional[str] - """Attempts to generate a m3u8 URL from the given flv URL""" - xbmc.log(str(flv)) - try: - return DW_VIDEO_TEMPLATE.format(flv.split("flv/")[1].split("vp6")[0]) - except IndexError: - return None - - -def get_search_url(query=None, tid=None, pid=None): - """Gets a full URL for a search page""" - language = SEARCH_LANGUAGE - query = "" if query is None else "&filter={}".format(query) - tid = "" if tid is None else "&themes={}".format(tid) - pid = "" if pid is None else "&programs={}".format(pid) - return DW_SEARCH_TEMPLATE.format(language, SEARCH_MAX_RESULTS, tid, pid, query) - - -def get_hidden(url): - # type: (str) -> int - """Attempts to extract the hide parameter value from a given URL""" - hidden = re.search(r"hide=(\d+)", url) - return int(hidden.group(1)) if hidden else 0 - - -def update_hidden(url, hidden=0): - # type: (str, int) -> str - """Updates or appends the 'hide' parameter for a URL""" - pattern = r"hide=(\d+)" - return re.sub(pattern, "hide={}".format(hidden), url) \ - if re.search(pattern, url) \ - else "{}&hide={}".format(url, hidden) - - -def cache_clear(): - # type: () -> None - """Clear the cache of all data""" - with Cache() as c: - c.clear() - - -def get_html(url): - # type: (str) -> Optional[BeautifulSoup] - """Gets cached or live HTML from the url""" - headers = { - "Accept": "text/html", - "Accept-encoding": "gzip" - } - with Cache() as c: - cached = c.get(url) - if cached: - if cached["fresh"]: - return BeautifulSoup(cached["blob"], "html.parser") - headers.update(conditional_headers(cached)) - r = requests.get(url, headers=headers, timeout=SEARCH_TIMEOUT) - if 200 == r.status_code: - soup = BeautifulSoup(r.content, "html.parser") - # c.set(url, r.content, r.headers) - return soup - if 304 == r.status_code: - c.touch(url, r.headers) - return BeautifulSoup(cached["blob"], "html.parser") - logger.debug("get_html error: {} {}".format(r.status_code, url)) - return None From 084971e8aa3a4f525055cf06e2a11411e249a87b Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Sun, 15 Feb 2026 22:04:09 -0500 Subject: [PATCH 5/9] The initial implementation of the OnDemand section was completed. --- resources/lib/EndPoints.py | 7 +++ resources/lib/OnDemand.py | 103 +++++++++++++++++++++++++++++++++++++ resources/lib/endpoints.py | 4 -- resources/lib/main.py | 34 ++++++------ 4 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 resources/lib/EndPoints.py create mode 100644 resources/lib/OnDemand.py delete mode 100644 resources/lib/endpoints.py diff --git a/resources/lib/EndPoints.py b/resources/lib/EndPoints.py new file mode 100644 index 0000000..e818f8f --- /dev/null +++ b/resources/lib/EndPoints.py @@ -0,0 +1,7 @@ +class EndPoints: + BASE_URL = "https://api.dw.com/api" + NAVIGATION = f"{BASE_URL}/navigation/{{language}}?product=smarttv&platform=androidtv" + VIDEO_DETAIL = f"{BASE_URL}/detail/video/{{video_id}}" + TOPICS = f"{BASE_URL}/epg/programgroups/topics/{{language_id}}" + VIDEO_LIST = f"{BASE_URL}/list/video/recent/{{language_id}}/program/{{program_id}}?pageIndex={{page_number}}" + PROGRAM_LIST = f"{BASE_URL}/epg/list/program/{{language_id}}" \ No newline at end of file diff --git a/resources/lib/OnDemand.py b/resources/lib/OnDemand.py new file mode 100644 index 0000000..063c43f --- /dev/null +++ b/resources/lib/OnDemand.py @@ -0,0 +1,103 @@ +from codequick import Route, Listitem, run, Script, utils, Resolver +from resources.lib.EndPoints import EndPoints +import simplejson as json +import requests + +class OnDemand: + @Route.register + def get_topics(plugin, language_id): + item = Listitem() + item.label = "Todos los programas" + item.set_callback(OnDemand.get_program_list, language_id=language_id) + yield item + + resp = requests.get(EndPoints.TOPICS.format(language_id=language_id)) + + if resp.status_code == 200: + topics = json.loads(resp.text) + for topic in topics: + item = Listitem() + + # The image tag contains both the image url and title + # img = elem.find(".//img") + + # Set the thumbnail image + # item.art["thumb"] = img.get("src") + + # Set the title + item.label = topic["name"] + + item.set_callback(OnDemand.get_program_list, language_id=language_id, programs_filter=topic.get("programIds")) + + # Return the listitem as a generator. + yield item + + @Route.register + def get_program_list(plugin, language_id, programs_filter=None): + resp = requests.get(EndPoints.PROGRAM_LIST.format(language_id=language_id)) + + if resp.status_code == 200: + programs = json.loads(resp.text) + programs_filter_set = set(programs_filter) if programs_filter else None + for program in programs: + if programs_filter_set is not None and program.get("id") not in programs_filter_set: + continue + item = Listitem() + + # Set the thumbnail image + sizes_image = program.get("image", {}).get("sizes", []) + if sizes_image: + item.art["thumb"] = sizes_image[-1]["url"] + + sizes_background = program.get("backgroundImage", {}).get("sizes", []) + if sizes_background: + item.art["fanart"] = sizes_background[-1]["url"] + else: + item.art["fanart"] = sizes_image[-1]["url"] + + # Set the title + item.label = program["name"] + item.info["plot"] = program["teaser"] + + program_id = program["id"] + item.set_callback(OnDemand.get_video_list, language_id=language_id, program_id=program_id) + + # Return the listitem as a generator. + yield item + + @Route.register + def get_video_list(plugin, language_id, program_id, page_number=1): + resp = requests.get(EndPoints.VIDEO_LIST.format(program_id=program_id,language_id=language_id,page_number=page_number)) + + if resp.status_code == 200: + videos = json.loads(resp.text) + for video in videos["items"]: + + item = Listitem() + sizes_image = video.get("image", {}).get("sizes", []) + if sizes_image: + item.art["thumb"] = sizes_image[-1]["url"] + item.art["fanart"] = sizes_image[-1]["url"] + + item.label = video["name"] + item.info["plot"] = video["teaserText"] + item.info["duration"] = video["duration"] + item.info["date"] = video["displayDate"] + + video_id=video["reference"]["id"] + item.set_callback(OnDemand.play_video, video_id=video_id) + + yield item + + @Resolver.register + def play_video(plugin, video_id): + resp = requests.get(EndPoints.VIDEO_DETAIL.format(video_id=video_id)) + + if resp.status_code == 200: + video = json.loads(resp.text) + sources = (video.get("mainContent") or {}).get("sources") or [] + for s in sources: + fmt = (s.get("format") or "").lower() + if fmt == "hls": + return s.get("url") + return None \ No newline at end of file diff --git a/resources/lib/endpoints.py b/resources/lib/endpoints.py deleted file mode 100644 index 3790719..0000000 --- a/resources/lib/endpoints.py +++ /dev/null @@ -1,4 +0,0 @@ -class Endpoints: - BASE_URL = "https://api.dw.com/api" - NAVIGATION = f"{BASE_URL}/navigation/{{language}}?product=smarttv&platform=androidtv" - VIDEO_DETAILS = f"{BASE_URL}/detail/video/{{video_id}}" \ No newline at end of file diff --git a/resources/lib/main.py b/resources/lib/main.py index d1338f5..3f450a5 100644 --- a/resources/lib/main.py +++ b/resources/lib/main.py @@ -1,36 +1,32 @@ from codequick import Route, Listitem, run, Script, utils -from resources.lib.endpoints import Endpoints +from resources.lib.EndPoints import EndPoints +from resources.lib.OnDemand import OnDemand import simplejson as json import requests @Route.register def root(plugin): - resp = requests.get(Endpoints.NAVIGATION.format(language="es_ES")) + language_id = 28 + resp = requests.get(EndPoints.NAVIGATION.format(language="es_ES")) if resp.status_code == 200: root_elem = json.loads(resp.text) # Parse each category for elem in root_elem["items"]: - item = Listitem() + if elem["type"] == "ProgramGroups": + item = Listitem() - # The image tag contains both the image url and title - # img = elem.find(".//img") + # The image tag contains both the image url and title + # img = elem.find(".//img") - # Set the thumbnail image - # item.art["thumb"] = img.get("src") + # Set the thumbnail image + # item.art["thumb"] = img.get("src") - # Set the title - item.label = elem["name"] + # Set the title + item.label = elem["name"] - # Fetch the url - # url = elem.find("div/a").get("href") + item.set_callback(OnDemand.get_topics, language_id=language_id) - # This will set the callback that will be called when listitem is activated. - # 'video_list' is the route callback function that we will create later. - # The 'url' argument is the url of the category that will be passed - # to the 'video_list' callback. - # item.set_callback(video_list, url=url) - - # Return the listitem as a generator. - yield item \ No newline at end of file + # Return the listitem as a generator. + yield item \ No newline at end of file From 59017f7fe00fad734556f4c174f078e55861aafc Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Sun, 15 Feb 2026 22:14:25 -0500 Subject: [PATCH 6/9] update version --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 2d21590..c0ceba3 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + From 48534e6bb905e141fc1048777532da6b95e91671 Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Sun, 22 Feb 2026 12:55:12 -0500 Subject: [PATCH 7/9] migrado al api graphql --- addon.xml | 2 +- .../resource.language.en_gb/strings.po | 8 +- resources/lib/OnDemand.py | 125 +++++---------- resources/lib/dw_api.py | 21 +++ resources/lib/dw_service.py | 42 +++++ resources/lib/gql_queries.py | 150 ++++++++++++++++++ resources/lib/main.py | 39 ++--- resources/settings.xml | 2 +- 8 files changed, 275 insertions(+), 114 deletions(-) create mode 100644 resources/lib/dw_api.py create mode 100644 resources/lib/dw_service.py create mode 100644 resources/lib/gql_queries.py diff --git a/addon.xml b/addon.xml index c0ceba3..785300e 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f7d8f12..5780fb6 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -53,8 +53,8 @@ msgid "Topics" msgstr "" msgctxt "#32009" -msgid "Programs" -msgstr "" +msgid "Shows" +msgstr "Shows" msgctxt "#32010" msgid "Settings" @@ -107,3 +107,7 @@ msgstr "" msgctxt "#32022" msgid "Are you sure?" msgstr "" + +msgctxt "#32023" +msgid "Podcast" +msgstr "" diff --git a/resources/lib/OnDemand.py b/resources/lib/OnDemand.py index 063c43f..2502ee2 100644 --- a/resources/lib/OnDemand.py +++ b/resources/lib/OnDemand.py @@ -1,103 +1,54 @@ from codequick import Route, Listitem, run, Script, utils, Resolver +from resources.lib.dw_api import DWGraphQL +from resources.lib.dw_service import DWService from resources.lib.EndPoints import EndPoints import simplejson as json import requests -class OnDemand: - @Route.register - def get_topics(plugin, language_id): - item = Listitem() - item.label = "Todos los programas" - item.set_callback(OnDemand.get_program_list, language_id=language_id) - yield item - - resp = requests.get(EndPoints.TOPICS.format(language_id=language_id)) - - if resp.status_code == 200: - topics = json.loads(resp.text) - for topic in topics: - item = Listitem() - - # The image tag contains both the image url and title - # img = elem.find(".//img") - - # Set the thumbnail image - # item.art["thumb"] = img.get("src") - - # Set the title - item.label = topic["name"] - - item.set_callback(OnDemand.get_program_list, language_id=language_id, programs_filter=topic.get("programIds")) - - # Return the listitem as a generator. - yield item +api = DWGraphQL() +svc = DWService(api) +class OnDemand: @Route.register - def get_program_list(plugin, language_id, programs_filter=None): - resp = requests.get(EndPoints.PROGRAM_LIST.format(language_id=language_id)) - - if resp.status_code == 200: - programs = json.loads(resp.text) - programs_filter_set = set(programs_filter) if programs_filter else None - for program in programs: - if programs_filter_set is not None and program.get("id") not in programs_filter_set: - continue - item = Listitem() + def get_program_list(plugin, language_id, content_type): + content_type = "videoPrograms" if content_type == "video" else "audioPrograms" + shows = svc.get_show_list(language_id)[content_type] + for show in shows: + item = Listitem() + item.label = show.get("name", "") + item.info["plot"] = show.get("teaser") or "" - # Set the thumbnail image - sizes_image = program.get("image", {}).get("sizes", []) - if sizes_image: - item.art["thumb"] = sizes_image[-1]["url"] - - sizes_background = program.get("backgroundImage", {}).get("sizes", []) - if sizes_background: - item.art["fanart"] = sizes_background[-1]["url"] - else: - item.art["fanart"] = sizes_image[-1]["url"] + img = (show.get("mainContentImage") or {}).get("staticUrl") + img = img.replace("${formatId}", "604") + if img: + item.art["thumb"] = img + item.art["fanart"] = img - # Set the title - item.label = program["name"] - item.info["plot"] = program["teaser"] + program_id = show.get("id") - program_id = program["id"] - item.set_callback(OnDemand.get_video_list, language_id=language_id, program_id=program_id) + item.set_callback(OnDemand.get_video_list, program_id=program_id) - # Return the listitem as a generator. - yield item + yield item @Route.register - def get_video_list(plugin, language_id, program_id, page_number=1): - resp = requests.get(EndPoints.VIDEO_LIST.format(program_id=program_id,language_id=language_id,page_number=page_number)) - - if resp.status_code == 200: - videos = json.loads(resp.text) - for video in videos["items"]: - - item = Listitem() - sizes_image = video.get("image", {}).get("sizes", []) - if sizes_image: - item.art["thumb"] = sizes_image[-1]["url"] - item.art["fanart"] = sizes_image[-1]["url"] - - item.label = video["name"] - item.info["plot"] = video["teaserText"] - item.info["duration"] = video["duration"] - item.info["date"] = video["displayDate"] - - video_id=video["reference"]["id"] - item.set_callback(OnDemand.play_video, video_id=video_id) - - yield item + def get_video_list(plugin, program_id): + videos = svc.get_show_episodes(program_id) + for video in videos: + item = Listitem() + item.label = video.get("name", "") + item.info["plot"] = video.get("teaser") or "" + item.info["duration"] = video.get("duration") or "" + item.info["premiered"] = video.get("contentDate") or "" + + img = video.get("posterImageUrl") or {} + if img: + item.art["thumb"] = img + item.art["fanart"] = img + + video_id = video.get("id", "") + item.set_callback(OnDemand.play_video, video_id=video_id) + yield item @Resolver.register def play_video(plugin, video_id): - resp = requests.get(EndPoints.VIDEO_DETAIL.format(video_id=video_id)) - - if resp.status_code == 200: - video = json.loads(resp.text) - sources = (video.get("mainContent") or {}).get("sources") or [] - for s in sources: - fmt = (s.get("format") or "").lower() - if fmt == "hls": - return s.get("url") - return None \ No newline at end of file + return svc.get_video_details(video_id).get("hlsVideoSrc") or svc.get_video_details(video_id).get("mp3Src") \ No newline at end of file diff --git a/resources/lib/dw_api.py b/resources/lib/dw_api.py new file mode 100644 index 0000000..9624f73 --- /dev/null +++ b/resources/lib/dw_api.py @@ -0,0 +1,21 @@ +import requests + +class DWGraphQL: + BASE = "https://webapi.dw.com/graphql" + + def __init__(self, session=None, timeout=60): + self.s = session or requests.Session() + self.timeout = timeout + + def execute(self, operation_name: str, query: str, variables: dict): + payload = { + "operationName": operation_name, + "query": query, + "variables": variables + } + r = self.s.post(self.BASE, json=payload, timeout=self.timeout) + r.raise_for_status() + data = r.json() + if "errors" in data: + raise RuntimeError(data["errors"]) + return data["data"] \ No newline at end of file diff --git a/resources/lib/dw_service.py b/resources/lib/dw_service.py new file mode 100644 index 0000000..cedc63d --- /dev/null +++ b/resources/lib/dw_service.py @@ -0,0 +1,42 @@ +from resources.lib.dw_api import DWGraphQL +from resources.lib.gql_queries import * + +app_name="kodi" + +class DWService: + def __init__(self, api: DWGraphQL): + self.api = api + + def get_show_list(self, language_id): + variables = {"lang": language_id, "appName": app_name} + data = self.api.execute("getShowList", GET_SHOW_LIST, variables) + + overview = (data.get("programsOverview") or {}) + video = overview.get("videoPrograms") or [] + audio = overview.get("audioPrograms") or [] + return {"videoPrograms": video, "audioPrograms": audio} + + def get_show_episodes(self, show_id: int): + variables = { + "keys": [{"id": int(show_id), "type": "UNIFIED_PROGRAM"}], + "amount": 16, + "appName": app_name, + } + + data = self.api.execute("getShowEpisodes", GET_SHOWS_EPISODES, variables) + + contents = data.get("contents") or [] + if not contents: + return [] + + return (contents[0] or {}).get("moreContentsFromUnifiedProgram") or [] + + def get_video_details(self, tracking_id: int): + variables = {"id": tracking_id, "app_name": app_name} + data = self.api.execute("getVideoDetails", GET_VIDEO_DETAILS, variables) + + content = data.get("content") or [] + if not content: + return [] + + return content \ No newline at end of file diff --git a/resources/lib/gql_queries.py b/resources/lib/gql_queries.py new file mode 100644 index 0000000..2809f84 --- /dev/null +++ b/resources/lib/gql_queries.py @@ -0,0 +1,150 @@ +GET_SHOW_LIST = """ +query getShowList($lang: Language!) { + programsOverview(lang: $lang) { + audioPrograms { ...ShowFields } + videoPrograms { ...ShowFields } + } +} +fragment ShowFields on UnifiedProgram { + id + name + title + teaser + text + isRtl + mainContentImage { staticUrl } + categories { originId name } + namedUrl + departments { name } + trackingId + trackingDate + trackingTopicsCommaJoined + language +} +""" + +GET_SHOWS_EPISODES = """ +query getShowEpisodes($keys: [ContentKeyInput]!, $amount: Int!) { + __typename contents(keys: $keys) { + __typename + ... on UnifiedProgram { + moreContentsFromUnifiedProgram(amount: $amount, types: [VIDEO, AUDIO]) { + __typename + ... on Audio { + ... AudioFields + } + ... on Video { + ... VideoFields + } + } + } + } +} + +fragment VideoFields on Video { + __typename id name title teaser duration posterImageUrl isRtl regions { + __typename name regionCode countryCode + } + categories { + __typename originId name + } + duration contentDate mainContentImage { + __typename staticUrl + } + unifiedPrograms(amount: 1) { + __typename id name autoDelivery mainContentImage { + __typename staticUrl + } + } + namedUrl departments { + __typename name + } + trackingId trackingDate trackingTopicsCommaJoined trackingRegionsCommaJoined language subtitles { + __typename language subtitleUrl + } +} + +fragment AudioFields on Audio { + __typename id name title teaser duration posterImageUrl isRtl regions { + __typename name regionCode countryCode + } + categories { + __typename originId name + } + duration contentDate mainContentImage { + __typename staticUrl + } + unifiedPrograms(amount: 1) { + __typename id name autoDelivery mainContentImage { + __typename staticUrl + } + } + namedUrl departments { + __typename name + } + trackingId trackingDate trackingTopicsCommaJoined trackingRegionsCommaJoined language +} +""" + +GET_VIDEO_DETAILS = """ +query getVideoDetails($id: Int!) { + __typename content(id: $id) { + __typename + ... on Video { + ... VideoFields hlsVideoSrc moreAvContentsByThematicFocusAndGlobal { + __typename + ... on Video { + ... VideoFields + } + } + } + ... on Audio { + ... AudioFields mp3Src + } + } +} + +fragment VideoFields on Video { + __typename id name title teaser duration posterImageUrl isRtl regions { + __typename name regionCode countryCode + } + categories { + __typename originId name + } + duration contentDate mainContentImage { + __typename staticUrl + } + unifiedPrograms(amount: 1) { + __typename id name autoDelivery mainContentImage { + __typename staticUrl + } + } + namedUrl departments { + __typename name + } + trackingId trackingDate trackingTopicsCommaJoined trackingRegionsCommaJoined language subtitles { + __typename language subtitleUrl + } +} + +fragment AudioFields on Audio { + __typename id name title teaser duration posterImageUrl isRtl regions { + __typename name regionCode countryCode + } + categories { + __typename originId name + } + duration contentDate mainContentImage { + __typename staticUrl + } + unifiedPrograms(amount: 1) { + __typename id name autoDelivery mainContentImage { + __typename staticUrl + } + } + namedUrl departments { + __typename name + } + trackingId trackingDate trackingTopicsCommaJoined trackingRegionsCommaJoined language +} +""" \ No newline at end of file diff --git a/resources/lib/main.py b/resources/lib/main.py index 3f450a5..e3cdb2e 100644 --- a/resources/lib/main.py +++ b/resources/lib/main.py @@ -3,30 +3,23 @@ from resources.lib.OnDemand import OnDemand import simplejson as json import requests +import xbmcaddon -@Route.register -def root(plugin): - language_id = 28 - resp = requests.get(EndPoints.NAVIGATION.format(language="es_ES")) - - if resp.status_code == 200: - root_elem = json.loads(resp.text) - - # Parse each category - for elem in root_elem["items"]: - if elem["type"] == "ProgramGroups": - item = Listitem() +language_id = Script.setting["language"].upper() +addon = xbmcaddon.Addon() +_ = addon.getLocalizedString - # The image tag contains both the image url and title - # img = elem.find(".//img") - # Set the thumbnail image - # item.art["thumb"] = img.get("src") - - # Set the title - item.label = elem["name"] - - item.set_callback(OnDemand.get_topics, language_id=language_id) +@Route.register +def root(plugin): + # Shows + item = Listitem() + item.label = _(32009) + item.set_callback(OnDemand.get_program_list, language_id=language_id,content_type="video") + yield item - # Return the listitem as a generator. - yield item \ No newline at end of file + # Podcast + item = Listitem() + item.label = _(32023) + item.set_callback(OnDemand.get_program_list, language_id=language_id, content_type="audio") + yield item \ No newline at end of file diff --git a/resources/settings.xml b/resources/settings.xml index 0ad12c0..3849e85 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,6 +1,7 @@ + @@ -14,7 +15,6 @@ - From d3580143b01bf8fd8549a668087a489079e76b1c Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Sun, 22 Feb 2026 16:14:12 -0500 Subject: [PATCH 8/9] add live tv --- .../resource.language.de_de/strings.po | 113 ++++++++++++++++++ .../resource.language.en_gb/strings.po | 2 +- .../resource.language.es_es/strings.po | 113 ++++++++++++++++++ resources/lib/LiveTv.py | 46 +++++++ resources/lib/dw_service.py | 12 +- resources/lib/gql_queries.py | 20 ++++ resources/lib/main.py | 9 +- 7 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 resources/language/resource.language.de_de/strings.po create mode 100644 resources/language/resource.language.es_es/strings.po create mode 100644 resources/lib/LiveTv.py diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000..9629c55 --- /dev/null +++ b/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,113 @@ +# Kodi Media Center language file +# Addon Name: Deutsche Welle +# Addon id: plugin.video.dw +# Addon Provider: fraser +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "General" +msgstr "" + +msgctxt "#32001" +msgid "Debug" +msgstr "" + +msgctxt "#32002" +msgid "Language" +msgstr "Sprache" + +msgctxt "#32003" +msgid "!UNUSED!" +msgstr "" + +msgctxt "#32004" +msgid "Past 24 hours" +msgstr "" + +msgctxt "#32005" +msgid "Recent" +msgstr "" + +msgctxt "#32006" +msgid "Live" +msgstr "Live" + +msgctxt "#32007" +msgid "Search" +msgstr "" + +msgctxt "#32008" +msgid "Topics" +msgstr "" + +msgctxt "#32009" +msgid "Shows" +msgstr "Shows" + +msgctxt "#32010" +msgid "Settings" +msgstr "" + +msgctxt "#32011" +msgid "Page" +msgstr "" + +msgctxt "#32012" +msgid "Menu" +msgstr "" + +msgctxt "#32013" +msgid "Results per-page" +msgstr "" + +msgctxt "#32014" +msgid "Clear Recently Viewed" +msgstr "" + +msgctxt "#32015" +msgid "Clear Searches" +msgstr "" + +msgctxt "#32016" +msgid "New Search" +msgstr "" + +msgctxt "#32017" +msgid "Cache" +msgstr "" + +msgctxt "#32018" +msgid "Clear Cache" +msgstr "" + +msgctxt "#32019" +msgid "Remove Search" +msgstr "" + +msgctxt "#32020" +msgid "Save Searches" +msgstr "" + +msgctxt "#32021" +msgid "!!UNUSED!!" +msgstr "" + +msgctxt "#32022" +msgid "Are you sure?" +msgstr "" + +msgctxt "#32023" +msgid "Podcast" +msgstr "Podcast" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5780fb6..26aa6b7 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -41,7 +41,7 @@ msgid "Recent" msgstr "" msgctxt "#32006" -msgid "Live TV" +msgid "Live" msgstr "" msgctxt "#32007" diff --git a/resources/language/resource.language.es_es/strings.po b/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000..eb9bae0 --- /dev/null +++ b/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,113 @@ +# Kodi Media Center language file +# Addon Name: Deutsche Welle +# Addon id: plugin.video.dw +# Addon Provider: fraser +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "General" +msgstr "" + +msgctxt "#32001" +msgid "Debug" +msgstr "" + +msgctxt "#32002" +msgid "Language" +msgstr "Idioma" + +msgctxt "#32003" +msgid "!UNUSED!" +msgstr "" + +msgctxt "#32004" +msgid "Past 24 hours" +msgstr "" + +msgctxt "#32005" +msgid "Recent" +msgstr "" + +msgctxt "#32006" +msgid "Live" +msgstr "En Vivo" + +msgctxt "#32007" +msgid "Search" +msgstr "" + +msgctxt "#32008" +msgid "Topics" +msgstr "" + +msgctxt "#32009" +msgid "Shows" +msgstr "Programas" + +msgctxt "#32010" +msgid "Settings" +msgstr "" + +msgctxt "#32011" +msgid "Page" +msgstr "" + +msgctxt "#32012" +msgid "Menu" +msgstr "" + +msgctxt "#32013" +msgid "Results per-page" +msgstr "" + +msgctxt "#32014" +msgid "Clear Recently Viewed" +msgstr "" + +msgctxt "#32015" +msgid "Clear Searches" +msgstr "" + +msgctxt "#32016" +msgid "New Search" +msgstr "" + +msgctxt "#32017" +msgid "Cache" +msgstr "" + +msgctxt "#32018" +msgid "Clear Cache" +msgstr "" + +msgctxt "#32019" +msgid "Remove Search" +msgstr "" + +msgctxt "#32020" +msgid "Save Searches" +msgstr "" + +msgctxt "#32021" +msgid "!!UNUSED!!" +msgstr "" + +msgctxt "#32022" +msgid "Are you sure?" +msgstr "" + +msgctxt "#32023" +msgid "Podcast" +msgstr "Podcast" diff --git a/resources/lib/LiveTv.py b/resources/lib/LiveTv.py new file mode 100644 index 0000000..048ee2f --- /dev/null +++ b/resources/lib/LiveTv.py @@ -0,0 +1,46 @@ +from codequick import Route, Listitem, run, Script, utils, Resolver +from resources.lib.dw_api import DWGraphQL +from resources.lib.dw_service import DWService +from resources.lib.EndPoints import EndPoints +import simplejson as json +import requests + +api = DWGraphQL() +svc = DWService(api) + +class LiveTv: + @Route.register + def get_live_tv_channels(plugin): + channels = svc.get_live_tv() + for channel in channels: + item = Listitem() + item.label = channel.get("name", "") + timeslots = channel.get("nextTimeSlots", []) + if timeslots: + first_slot = timeslots[0] + program = first_slot.get("program", {}) + program_element = first_slot.get("programElement", {}) + + title = program.get("title", "") + subtitle = program.get("subTitle", "").strip() + teaser = program.get("teaser", "") + + # Construir plot bonito tipo EPG + plot = f"[B]{title}[/B]\n" + if subtitle: + plot += f"{subtitle}\n\n" + + plot += teaser + + item.info["plot"] = plot + + # Imagen + image = program.get("mainContentImage", {}) + if image: + img_url = image.get("staticUrl", "").replace("${formatId}", "605") + item.art["thumb"] = img_url + item.art["fanart"] = img_url + + item.property["IsLive"] = "true" + item.set_path(channel.get("livestreamUrl")) + yield item \ No newline at end of file diff --git a/resources/lib/dw_service.py b/resources/lib/dw_service.py index cedc63d..e5d9b41 100644 --- a/resources/lib/dw_service.py +++ b/resources/lib/dw_service.py @@ -39,4 +39,14 @@ def get_video_details(self, tracking_id: int): if not content: return [] - return content \ No newline at end of file + return content + + def get_live_tv(self): + variables = {"channelNames": [], "app_name": app_name} + data = self.api.execute("getLiveTV", GET_LIVE_TV, variables) + + livestream_channels = data.get("livestreamChannels") or [] + if not livestream_channels: + return [] + + return livestream_channels \ No newline at end of file diff --git a/resources/lib/gql_queries.py b/resources/lib/gql_queries.py index 2809f84..64cbfa5 100644 --- a/resources/lib/gql_queries.py +++ b/resources/lib/gql_queries.py @@ -147,4 +147,24 @@ } trackingId trackingDate trackingTopicsCommaJoined trackingRegionsCommaJoined language } +""" + +GET_LIVE_TV = """ +query getLiveTV($channelNames: [ChannelNames]!) { + __typename livestreamChannels(channelNames: $channelNames) { + __typename id language name title livestreamUrl isRtl nextTimeSlots { + __typename startDate endDate isRtl program { + __typename title subTitle teaser mainContentImage { + __typename staticUrl + } + } + programElement { + __typename title teaser + } + } + namedUrl trackingId trackingDate mainContentImage { + __typename staticUrl + } + } +} """ \ No newline at end of file diff --git a/resources/lib/main.py b/resources/lib/main.py index e3cdb2e..2950aaa 100644 --- a/resources/lib/main.py +++ b/resources/lib/main.py @@ -1,6 +1,7 @@ from codequick import Route, Listitem, run, Script, utils from resources.lib.EndPoints import EndPoints from resources.lib.OnDemand import OnDemand +from resources.lib.LiveTv import LiveTv import simplejson as json import requests import xbmcaddon @@ -11,7 +12,13 @@ @Route.register -def root(plugin): +def root(plugin): + # Live TV + item = Listitem() + item.label = _(32006) + item.set_callback(LiveTv.get_live_tv_channels) + yield item + # Shows item = Listitem() item.label = _(32009) From 8eca82c06342e9897d8fc12b6e4355150a9d7d60 Mon Sep 17 00:00:00 2001 From: Gigoro33 Date: Sun, 22 Feb 2026 16:15:43 -0500 Subject: [PATCH 9/9] update version --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 785300e..eed4c85 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - +