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 5570fd1..eed4c85 100644 --- a/addon.xml +++ b/addon.xml @@ -1,13 +1,11 @@ - + - - - - - + + + - + video @@ -22,6 +20,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/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/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 f7d8f12..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" @@ -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/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/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/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/OnDemand.py b/resources/lib/OnDemand.py new file mode 100644 index 0000000..2502ee2 --- /dev/null +++ b/resources/lib/OnDemand.py @@ -0,0 +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 + +api = DWGraphQL() +svc = DWService(api) + +class OnDemand: + @Route.register + 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 "" + + img = (show.get("mainContentImage") or {}).get("staticUrl") + img = img.replace("${formatId}", "604") + if img: + item.art["thumb"] = img + item.art["fanart"] = img + + program_id = show.get("id") + + item.set_callback(OnDemand.get_video_list, program_id=program_id) + + yield item + + @Route.register + 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): + 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..e5d9b41 --- /dev/null +++ b/resources/lib/dw_service.py @@ -0,0 +1,52 @@ +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 + + 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 new file mode 100644 index 0000000..64cbfa5 --- /dev/null +++ b/resources/lib/gql_queries.py @@ -0,0 +1,170 @@ +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 +} +""" + +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/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..2950aaa --- /dev/null +++ b/resources/lib/main.py @@ -0,0 +1,32 @@ +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 + +language_id = Script.setting["language"].upper() +addon = xbmcaddon.Addon() +_ = addon.getLocalizedString + + +@Route.register +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) + item.set_callback(OnDemand.get_program_list, language_id=language_id,content_type="video") + yield item + + # 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/lib/plugin.py b/resources/lib/plugin.py deleted file mode 100644 index 1800dec..0000000 --- a/resources/lib/plugin.py +++ /dev/null @@ -1,309 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Main plugin file - Handles the various routes""" - -__author__ = "fraser" - -import logging - -import routing -import xbmc -import xbmcaddon -import xbmcplugin -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(): - 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) - - -@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("div", {"id": "bodyContent"}).extract() - items = content.find_all("div", "epg") - for item in items: - img = item.find("img") - title = item.find("h2").text.encode("utf-8") - action = item.find("a", string="All videos") - pid = dws.get_program_id(action.get("href")) - plot = item.find("p").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 24ba89c..0000000 --- a/resources/lib/search.py +++ /dev/null @@ -1,166 +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 - -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) -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 - -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 - return { - "path": path, - "image": get_url(preview_image.get("value")) if preview_image else "", - "info": { - "title": title, - "plot": plot.text if plot else title, - "duration": int(duration.get("value")) 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"programs=(\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""" - 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 = "en" if tid or pid else 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 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 @@ -