diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c5b57c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyo +.idea +.DS_Store \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4394beb --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Fraser Chapman (https://github.com/FraserChapman) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a099aa --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# plugin.video.dw + +This add-on enables playing of videos and movies from the [Deutsche Welle](https://www.dw.com/) website. + +* Shows Live TV such as; DW English, DW Deutsch, DW Deutsch+, DW Español and عربية DW +* Shows TV Programmes such as; Arts.21, Close Up, DocFilm, etc +* Shows topics such as; Lifestyle, Education, Science, Sports, etc +* Shows programs from the last 24 hours +* Allows searching and saved searches +* Caches recently viewed files for fast replay + +## Notes on language + +To view Arabic characters with the default Estuary skin you need to set the font to "Arial based" + +```Settings -> Interface -> Fonts "Arial based"``` + +The language option in the add-on settings applies to "Search" results and items in "Last 24 hours" section - "Topic" and "Programme" text is always in English (en). + +## The Collection + +Deutsche Welle is Germany’s international broadcaster. It conveys a comprehensive image of Germany, reports events and developments and incorporates German and other perspectives in a journalistically independent manner. + +## Disclaimer + +This add-on is not created, maintained or in any way affiliated with Deutsche Welle. It only provides an interface to access the free content on the Deutsche Welle website from Kodi. + +## Screen Shots + +![ss1](resources/media/ss1.jpg) +![ss2](resources/media/ss2.jpg) +![ss3](resources/media/ss3.jpg) +![ss3](resources/media/ss4.jpg) +![ss3](resources/media/ss5.jpg) + +## Licence + +All art work, code and data is provided under an [MIT License](LICENSE.txt) + +Except the two images icon.png and fanart.jpg + +![icon.png](resources/icon.png?) + +[Twitter - Public Domain / Fair use](https://twitter.com/DeutscheWelle) + +![fanart.jpg](resources/fanart.jpg) + +[Daily Motion - Public Domain / Fair Use](https://www.dailymotion.com/video/x3rb768) + +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/06fcd4045eaf4eb4982e28df04127500)](https://www.codacy.com/app/FraserChapman/plugin.video.dw?utm_source=github.com&utm_medium=referral&utm_content=FraserChapman/plugin.video.dw&utm_campaign=Badge_Grade) diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..5570fd1 --- /dev/null +++ b/addon.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + video + + + Search and play free content from the Deutsche Welle website. + Deutsche Welle is Germany’s international broadcaster. +Watch live TV and a range of programmes available via the DW website + en de es ar + all + MIT + https://forum.kodi.tv/showthread.php?tid=345450 + https://www.dw.com/ + fraser.chapman@gmail.com + https://github.com/FraserChapman/plugin.video.dw + v1.0.0 (14-7-19) - Initial version + Neither this addon nor its author are in anyway affiliated with Deutsche Welle + + resources/icon.png + resources/fanart.jpg + resources/media/ss1.jpg + resources/media/ss2.jpg + resources/media/ss3.jpg + resources/media/ss4.jpg + resources/media/ss5.jpg + + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..569dd47 --- /dev/null +++ b/changelog.txt @@ -0,0 +1 @@ +v1.0.0 - Initial version \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7c35ad7 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from resources.lib import plugin + +plugin.run() diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/fanart.jpg b/resources/fanart.jpg new file mode 100644 index 0000000..2364392 Binary files /dev/null and b/resources/fanart.jpg differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..de9bcd3 Binary files /dev/null and b/resources/icon.png differ diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000..f7d8f12 --- /dev/null +++ b/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,109 @@ +# 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 "" + +msgctxt "#32003" +msgid "!UNUSED!" +msgstr "" + +msgctxt "#32004" +msgid "Past 24 hours" +msgstr "" + +msgctxt "#32005" +msgid "Recent" +msgstr "" + +msgctxt "#32006" +msgid "Live TV" +msgstr "" + +msgctxt "#32007" +msgid "Search" +msgstr "" + +msgctxt "#32008" +msgid "Topics" +msgstr "" + +msgctxt "#32009" +msgid "Programs" +msgstr "" + +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 "" diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/lib/kodilogging.py b/resources/lib/kodilogging.py new file mode 100644 index 0000000..bd47221 --- /dev/null +++ b/resources/lib/kodilogging.py @@ -0,0 +1,42 @@ +# -*- 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 new file mode 100644 index 0000000..decee5f --- /dev/null +++ b/resources/lib/kodiutils.py @@ -0,0 +1,92 @@ +# -*- 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/plugin.py b/resources/lib/plugin.py new file mode 100644 index 0000000..1800dec --- /dev/null +++ b/resources/lib/plugin.py @@ -0,0 +1,309 @@ +# -*- 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 new file mode 100644 index 0000000..24ba89c --- /dev/null +++ b/resources/lib/search.py @@ -0,0 +1,166 @@ +# -*- 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/media/live.png b/resources/media/live.png new file mode 100644 index 0000000..12d3b60 Binary files /dev/null and b/resources/media/live.png differ diff --git a/resources/media/livetv.png b/resources/media/livetv.png new file mode 100644 index 0000000..f70d0fc Binary files /dev/null and b/resources/media/livetv.png differ diff --git a/resources/media/past24h.png b/resources/media/past24h.png new file mode 100644 index 0000000..fcb05e8 Binary files /dev/null and b/resources/media/past24h.png differ diff --git a/resources/media/programme.png b/resources/media/programme.png new file mode 100644 index 0000000..fb0efde Binary files /dev/null and b/resources/media/programme.png differ diff --git a/resources/media/recent.png b/resources/media/recent.png new file mode 100644 index 0000000..373929e Binary files /dev/null and b/resources/media/recent.png differ diff --git a/resources/media/search.png b/resources/media/search.png new file mode 100644 index 0000000..b4d9967 Binary files /dev/null and b/resources/media/search.png differ diff --git a/resources/media/settings.png b/resources/media/settings.png new file mode 100644 index 0000000..d579ba8 Binary files /dev/null and b/resources/media/settings.png differ diff --git a/resources/media/ss1.jpg b/resources/media/ss1.jpg new file mode 100644 index 0000000..d924434 Binary files /dev/null and b/resources/media/ss1.jpg differ diff --git a/resources/media/ss2.jpg b/resources/media/ss2.jpg new file mode 100644 index 0000000..779dcb1 Binary files /dev/null and b/resources/media/ss2.jpg differ diff --git a/resources/media/ss3.jpg b/resources/media/ss3.jpg new file mode 100644 index 0000000..186ad03 Binary files /dev/null and b/resources/media/ss3.jpg differ diff --git a/resources/media/ss4.jpg b/resources/media/ss4.jpg new file mode 100644 index 0000000..94934bc Binary files /dev/null and b/resources/media/ss4.jpg differ diff --git a/resources/media/ss5.jpg b/resources/media/ss5.jpg new file mode 100644 index 0000000..d4c7b2d Binary files /dev/null and b/resources/media/ss5.jpg differ diff --git a/resources/media/topic.png b/resources/media/topic.png new file mode 100644 index 0000000..4a24f76 Binary files /dev/null and b/resources/media/topic.png differ diff --git a/resources/media/year.png b/resources/media/year.png new file mode 100644 index 0000000..6fac9e3 Binary files /dev/null and b/resources/media/year.png differ diff --git a/resources/settings.xml b/resources/settings.xml new file mode 100644 index 0000000..0ad12c0 --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file