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..b9204d2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 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..621d6b5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# plugin.video.loc + +This add-on enables playing of videos and movies from the [Library of Congress](https://www.loc.gov/) website. + +* Shows collections such as; National Book Festivals, American Folklife Center, Event Videos, etc +* Shows collections by; Contributor, Subject, Location, etc +* Allows searching the archive and saved searches +* Caches recently viewed files for fast replay + +[Donations for this add-on gratefully accepted](https://www.paypal.me/fraserchapman) + +## The Collection + +The Library of Congress is the largest library in the world, with millions of books, recordings, photographs, newspapers, maps and manuscripts in its collections. +The Library is the main research arm of the U.S. Congress and the home of the U.S. Copyright Office. +Several thousand clips, videos and early motion pictures are viewable from the Library's Digital Collection. + +## Disclaimer + +This add-on is not created, maintained or in any way affiliated with the Library of Congress. +It only provides an interface to access the free content on the Library of Congress 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) +![ss3](resources/media/ss6.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) + +[Library of Congress - Fair use](https://www.loc.gov/) + +![fanart.jpg](resources/fanart.jpg) + +[10wallpaper - Fair Use](https://www.10wallpaper.com/view/Washington_Library_of_Congress-2017_Bing_Desktop_Wallpapers.html) + +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/56607a2f1c5e4b139919c3c2108f5d16)](https://www.codacy.com/app/FraserChapman/plugin.video.loc?utm_source=github.com&utm_medium=referral&utm_content=FraserChapman/plugin.video.loc&utm_campaign=Badge_Grade) diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..448be38 --- /dev/null +++ b/addon.xml @@ -0,0 +1,39 @@ + + + + + + + + + + video + + + Search and play free content from the Library of Congress + The Library is the main research arm of the U.S. Congress and the home of the U.S. Copyright Office. +Several thousand clips, videos and early motion pictures are viewable from the Library's Digital Collection. + + en + all + MIT + https://forum.kodi.tv/showthread.php?tid=345338 + https://www.loc.gov/ + fraser.chapman@gmail.com + https://github.com/FraserChapman/plugin.video.loc + v1.0.0 (11-7-19) - Initial version + Neither this addon nor its author are in anyway affiliated with the Library of + Congress + + + 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 + resources/media/ss6.jpg + + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..5239017 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,2 @@ +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..7b59d80 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..50efb95 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..bc46088 --- /dev/null +++ b/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,109 @@ +# Kodi Media Center language file +# Addon Name: Library of Congress +# Addon id: plugin.video.loc +# 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 "Contributor" +msgstr "" + +msgctxt "#32003" +msgid "Colour" +msgstr "" + +msgctxt "#32004" +msgid "Subject" +msgstr "" + +msgctxt "#32005" +msgid "Recent" +msgstr "" + +msgctxt "#32006" +msgid "Years" +msgstr "" + +msgctxt "#32007" +msgid "Search" +msgstr "" + +msgctxt "#32008" +msgid "Fiction" +msgstr "" + +msgctxt "#32009" +msgid "Location" +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 "Part of" +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..5341ac4 --- /dev/null +++ b/resources/lib/plugin.py @@ -0,0 +1,282 @@ +# -*- 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 locs + +kodilogging.config() +logger = logging.getLogger(__name__) +plugin = routing.Plugin() +ADDON_NAME = xbmcaddon.Addon().getAddonInfo("name") # Library of Congress + +LOC_PERIODS = [ + "1700-1799", + "1890-1899", + "1900-1909", + "1910-1919", + "1920-1929", + "1930-1939", + "1940-1949", + "1950-1959", + "1950-1959", + "1960-1969", + "1970-1979", + "1980-1989", + "1990-1999", + "2000-2009", + "2010-2019", + "2019-2029" # future proof +] + + +def parse_search_results(data, category): + """Adds menu items for search result data""" + paginate(data["pagination"], category) + for data in data["results"]: + add_menu_item(play_film, + data.get("title").title(), + {"href": "{}?fo=json".format(data.get("url"))}, + locs.get_art(data), + locs.get_info(data), + False) + xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) + xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_GENRE) + xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) + + +def paginate(json, category, idx=None): + """Adds pagination to results pages""" + if not json: + return + next_page = json.get("next", False) + current = int(json.get("current", 0)) + if current > 0: + add_menu_item(index, "[{}]".format(ku.localize(32012))) # [Menu] + if next_page: + offset = current + 1 + add_menu_item(section, + "[{} {}]".format(ku.localize(32011), offset), # [Page n+1] + { + "idx": idx, + "offset": offset, + "category": category + } if idx else { + "href": next_page, + "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 == play_film: + 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_contributor"): + add_menu_item(section, + 32002, + {"idx": "index/contributor/", "category": ku.localize(32002)}, + ku.icon("contributor.png")) + if ku.get_setting_as_bool("show_location"): + add_menu_item(section, + 32009, + {"idx": "index/location/", "category": ku.localize(32009)}, + ku.icon("location.png")) + if ku.get_setting_as_bool("show_partof"): + add_menu_item(section, + 32021, + {"idx": "index/partof/", "category": ku.localize(32021)}, + ku.icon("series.png")) + if ku.get_setting_as_bool("show_subject"): + add_menu_item(section, + 32004, + {"idx": "index/subject/", "category": ku.localize(32004)}, + ku.icon("genre.png")) + if ku.get_setting_as_bool("show_years"): + add_menu_item(year, 32006, art=ku.icon("year.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("/year") +def year(): + # type: () -> None + """Show year menu""" + for item in LOC_PERIODS: + url = locs.get_search_url(dates=item.replace("-", "/")) + add_menu_item(section, item, {"href": url, "category": item}) + xbmcplugin.setPluginCategory(plugin.handle, ku.localize(32006)) + xbmcplugin.endOfDirectory(plugin.handle) + + +@plugin.route("/section") +def section(): + # type: () -> None + """Show section menus and playable items""" + idx = get_arg("idx", False) + href = get_arg("href", False) + category = get_arg("category") + offset = get_arg("offset", 0) + if idx: + # Section menu + url = locs.get_search_url(idx, page=offset) + data = locs.get_json(url) + paginate(data.get("pagination"), category, idx) + for data in data.get("facets", [{}])[0].get("filters"): + title = data.get("title").title() + count = data.get("count") + add_menu_item(section, + "{} [{} items]".format(title, count), + {"href": data.get("on"), "category": title}) + if href: + # Playable items + data = locs.get_json(href) + parse_search_results(data, category) + xbmcplugin.setContent(plugin.handle, "videos") + xbmcplugin.setPluginCategory(plugin.handle, category) + xbmcplugin.endOfDirectory(plugin.handle) + + +@plugin.route("/recent") +def recent(): + # type: () -> None + """Show recently viewed films""" + data = locs.recents.retrieve() + for href in data: + data = locs.get_json(href) + add_menu_item( + play_film, + data.get("item", {}).get("title").title(), + {"href": href}, + locs.get_art(data), + locs.get_info(data), + 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""" + href = get_arg("href") + data = locs.get_json(href) + url = locs.get_video_url(data) + if not url: + logger.debug("play_film error: {}".format(href)) + return + locs.recents.append(href) + list_item = ListItem(path=url) + list_item.setInfo("video", locs.get_info(data)) + 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(): + locs.cache_clear() + if idx == "recent" and ku.confirm(): + locs.recents.clear() + if idx == "search" and ku.confirm(): + locs.searches.clear() + + +@plugin.route("/search") +def search(): + # type: () -> Optional[bool] + """Search the archive""" + query = get_arg("q") + category = get_arg("category", ku.localize(32007)) # Search + # Remove saved search item + if bool(get_arg("delete", False)): + locs.searches.remove(query) + xbmc.executebuiltin("Container.Refresh()") + return True + # View saved search menu + if bool(get_arg("menu", False)): + add_menu_item(search, "[{}]".format(ku.localize(32016)), {"new": True}) # [New Search] + for item in locs.searches.retrieve(): + text = item.encode("utf-8") + add_menu_item(search, text, {"q": text, "category": "{} '{}'".format(ku.localize(32007), text)}) + xbmcplugin.setPluginCategory(plugin.handle, category) + xbmcplugin.endOfDirectory(plugin.handle) + return True + # New look-up + if bool(get_arg("new", False)): + query = ku.user_input() + if not query: + return False + category = "{} '{}'".format(ku.localize(32007), query) + if locs.SEARCH_SAVED: + locs.searches.append(query) + # Process search + url = locs.get_search_url(query=query) + data = locs.get_json(url) + parse_search_results(data, category) + 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..5e6917e --- /dev/null +++ b/resources/lib/search.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +"""BP searcher and helpers""" + +__author__ = "fraser" + +import logging +import re + +import requests +from cache import Cache, Store, conditional_headers + +from . import kodiutils as ku + +LOC_URI = "https://www.loc.gov/" +LOS_MEDIA_TEMPLATE = "https://media.loc.gov/services/v1/media?id={}" +LOC_SEARCH_TEMPLATE = "{}film-and-videos/{{}}" \ + "?fa=online-format:video&fo=json&q={{}}&dates={{}}&c={{}}&sp={{}}".format(LOC_URI) +LOC_STATIC_IMAGE = "/static/images/original-format/film-video.png" +LOC_DEFAULT_IMAGE_URI = "{}{}".format(LOC_URI, LOC_STATIC_IMAGE.lstrip("/")) + +SEARCH_SAVED = ku.get_setting_as_bool("search_saved") +SEARCH_MAX_RESULTS = ku.get_setting("search_max_results") +SEARCH_TIMEOUT = 60 + +searches = Store("app://saved-searches") +recents = Store("app://recently-viewed") +logger = logging.getLogger(__name__) + + +def extract_year(text): + # type: (str) -> int + """Attempts to extract the first four digits in sequence from a string, defaults to 0""" + data = re.search(r"\d{4}", text) + return int(data.group()) if data else 0 + + +def get_art(data): + # type: (dict) -> dict + """Gets the art-work for the given result data""" + image = data.get("resources", [{}])[0].get("image") + if not image or image == LOC_STATIC_IMAGE: # default + return ku.art(LOC_DEFAULT_IMAGE_URI) + if image.startswith("//"): # no protocol + return ku.art("http:{}".format(image)) + return ku.art(image) + + +def get_info(data): + # type: (dict) -> dict + """Gets the video information for the given result data""" + item = data.get("item", {}) + plot = item.get("summary", data.get("description")) + if plot and isinstance(plot, list): + plot = plot[0] + # TODO : some-kind of duration calculation... + return { + "title": item.get("title", "").title(), + "plot": plot, + "year": extract_year(item.get("date", "")), + "genre": item.get("genre") + } + + +def get_search_url(index=None, query=None, dates=None, page=0): + # type: (str, str, str, int) -> str + """Constructs a search URL based on the query and current search settings""" + index = "" if index is None else index + query = "" if query is None else query + dates = "" if dates is None else dates + return LOC_SEARCH_TEMPLATE.format(index, query, dates, SEARCH_MAX_RESULTS, page) + + +def get_mime_property(data, key, mime): + # type: (list, str, str) -> str + """Attempts to get key data from a mime-type object""" + try: + return [item.get(key) for item in data if item.get("mimetype") == mime][0] + except (IndexError, KeyError): + logger.debug("get_mime_property error: {} {}".format(key, mime)) + return "" + + +def get_video_url(data): + # type: (dict) -> Optional[str] + """Attempts to get a playable URL from given response data""" + resource = data.get("resources", [{}])[0] + url = resource.get("video_stream") # try m3u8 + if not url: # try mp4 + files = resource.get("files")[0] + mp4 = get_mime_property(files, "url", "video/mp4") + url = "https:{}".format(mp4) if mp4 and mp4.startswith("//") else mp4 + if not url: # try x-video + idx = get_mime_property(files, "mediaObjectId", "application/x-video") + media = get_json(LOS_MEDIA_TEMPLATE.format(idx)) + derivative = media.get("mediaObject").get("derivatives")[0] + url = "https://{}/{}".format( + derivative.get("fqdn"), + derivative.get("derivativeMediaUrl").replace("mp4:", "")) + return url + + +def cache_clear(): + # type: () -> None + """Clear the cache of all data""" + with Cache() as c: + c.clear() + + +def get_json(url): + """Gets cached or live JSON from the url""" + headers = { + "Accept": "text/html", + "Accept-encoding": "gzip" + } + with Cache() as c: + cached = c.get(url) + if cached: + if cached["fresh"]: + return cached["blob"] + headers.update(conditional_headers(cached)) + r = requests.get(url, headers=headers, timeout=SEARCH_TIMEOUT) + if 200 == r.status_code: + c.set(url, r.json(), r.headers) + return r.json() + if 304 == r.status_code: + c.touch(url, r.headers) + return cached["blob"] + logger.debug("get_json error: {} {}".format(r.status_code, url)) + return None diff --git a/resources/media/contributor.png b/resources/media/contributor.png new file mode 100644 index 0000000..28dc86c Binary files /dev/null and b/resources/media/contributor.png differ diff --git a/resources/media/featured.png b/resources/media/featured.png new file mode 100644 index 0000000..7dc3b5b Binary files /dev/null and b/resources/media/featured.png differ diff --git a/resources/media/genre.png b/resources/media/genre.png new file mode 100644 index 0000000..e51f167 Binary files /dev/null and b/resources/media/genre.png differ diff --git a/resources/media/location.png b/resources/media/location.png new file mode 100644 index 0000000..b5a774d Binary files /dev/null and b/resources/media/location.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/series.png b/resources/media/series.png new file mode 100644 index 0000000..fb0efde Binary files /dev/null and b/resources/media/series.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..bf21b35 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..242f3c5 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..41ff355 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..3865e97 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..3195e48 Binary files /dev/null and b/resources/media/ss5.jpg differ diff --git a/resources/media/ss6.jpg b/resources/media/ss6.jpg new file mode 100644 index 0000000..3e11652 Binary files /dev/null and b/resources/media/ss6.jpg 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..95b1ded --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file