Advertisement
Guest User

Modified pvrartwork.py for Kodi

a guest
Mar 22nd, 2020
451
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 24.93 KB | None | 0 0
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3.  
  4. """
  5.    script.module.metadatautils
  6.    pvrartwork.py
  7.    Get metadata for Kodi PVR programs
  8. """
  9.  
  10. from utils import get_clean_image, DialogSelect, log_msg, extend_dict, ADDON_ID, download_artwork, normalize_string
  11. import xbmc
  12. import xbmcgui
  13. import xbmcvfs
  14. from difflib import SequenceMatcher as SM
  15. from operator import itemgetter
  16. import re
  17. from urllib import quote_plus
  18. import os
  19. from datetime import timedelta
  20.  
  21.  
  22. class PvrArtwork(object):
  23.     """get artwork for kodi pvr"""
  24.  
  25.     def __init__(self, metadatautils):
  26.         """Initialize - optionaly provide our base MetadataUtils class"""
  27.         self._mutils = metadatautils
  28.         self.cache = self._mutils.cache
  29.  
  30.     def get_pvr_artwork(self, title, channel, genre="", manual_select=False, ignore_cache=False):
  31.         """
  32.            collect full metadata and artwork for pvr entries
  33.            parameters: title (required)
  34.            channel: channel name (required)
  35.            year: year or date (optional)
  36.            genre: (optional)
  37.            the more optional parameters are supplied, the better the search results
  38.        """
  39.         details = {"art": {}}
  40.         # try cache first
  41.         cache_str = "pvr_artwork.%s.%s" % (title.lower(), channel.lower())
  42.         cache = self._mutils.cache.get(cache_str)
  43.         if cache and not manual_select and not ignore_cache:
  44.             log_msg("get_pvr_artwork - return data from cache - %s" % cache_str)
  45.             details = cache
  46.         else:
  47.             # no cache - start our lookup adventure
  48.             log_msg("get_pvr_artwork - no data in cache - start lookup - %s" % cache_str)
  49.  
  50.             # workaround for recordings
  51.             recordingdetails = self.lookup_local_recording(title, channel)
  52.             if recordingdetails and not (channel and genre):
  53.                 genre = recordingdetails["genre"]
  54.                 channel = recordingdetails["channel"]
  55.  
  56.             details["pvrtitle"] = title
  57.             details["pvrchannel"] = channel
  58.             details["pvrgenre"] = genre
  59.             details["cachestr"] = cache_str
  60.             details["media_type"] = ""
  61.             details["art"] = {}
  62.  
  63.             # filter genre unknown/other
  64.             if not genre or genre.split(" / ")[0] in xbmc.getLocalizedString(19499).split(" / "):
  65.                 details["genre"] = []
  66.                 genre = ""
  67.                 log_msg("genre is unknown so ignore....")
  68.             else:
  69.                 details["genre"] = genre.split(" / ")
  70.                 details["media_type"] = self.get_mediatype_from_genre(genre)
  71.             searchtitle = self.get_searchtitle(title, channel)
  72.  
  73.             # only continue if we pass our basic checks
  74.             filterstr = self.pvr_proceed_lookup(title, channel, genre, recordingdetails)
  75.             proceed_lookup = False if filterstr else True
  76.             if not proceed_lookup and manual_select:
  77.                 # warn user about active skip filter
  78.                 proceed_lookup = xbmcgui.Dialog().yesno(
  79.                     line1=self._mutils.addon.getLocalizedString(32027), line2=filterstr,
  80.                     heading=xbmc.getLocalizedString(750))
  81.  
  82.             if proceed_lookup:
  83.  
  84.                 # if manual lookup get the title from the user
  85.                 if manual_select:
  86.                     searchtitle = xbmcgui.Dialog().input(xbmc.getLocalizedString(16017), searchtitle,
  87.                                                          type=xbmcgui.INPUT_ALPHANUM).decode("utf-8")
  88.                     if not searchtitle:
  89.                         return
  90.  
  91.                 # if manual lookup and no mediatype, ask the user
  92.                 if manual_select and not details["media_type"]:
  93.                     yesbtn = self._mutils.addon.getLocalizedString(32042)
  94.                     nobtn = self._mutils.addon.getLocalizedString(32043)
  95.                     header = self._mutils.addon.getLocalizedString(32041)
  96.                     if xbmcgui.Dialog().yesno(header, header, yeslabel=yesbtn, nolabel=nobtn):
  97.                         details["media_type"] = "movie"
  98.                     else:
  99.                         details["media_type"] = "tvshow"
  100.  
  101.                 # append thumb from recordingdetails
  102.                 if recordingdetails and recordingdetails.get("thumbnail"):
  103.                     details["art"]["thumb"] = recordingdetails["thumbnail"]
  104.                 # lookup custom path
  105.                 details = extend_dict(details, self.lookup_custom_path(searchtitle, title))
  106.                 # lookup movie/tv library
  107.                 details = extend_dict(details, self.lookup_local_library(searchtitle, details["media_type"]))
  108.  
  109.                 # do internet scraping if enabled
  110.                 if self._mutils.addon.getSetting("pvr_art_scraper") == "true":
  111.  
  112.                     log_msg(
  113.                         "pvrart start scraping metadata for title: %s - media_type: %s" %
  114.                         (searchtitle, details["media_type"]))
  115.  
  116.                     # prefer tmdb scraper
  117.                     tmdb_result = self._mutils.get_tmdb_details(
  118.                         "", "", searchtitle, "", "", details["media_type"],
  119.                             manual_select=manual_select, ignore_cache=manual_select)
  120.                     log_msg("pvrart lookup for title: %s - TMDB result: %s" % (searchtitle, tmdb_result))
  121.                     if tmdb_result:
  122.                         details["media_type"] = tmdb_result["media_type"]
  123.                         details = extend_dict(details, tmdb_result)
  124.  
  125.                     # fallback to tvdb scraper
  126.                     if (not tmdb_result or (tmdb_result and not tmdb_result.get("art")) or
  127.                             details["media_type"] == "tvshow"):
  128.                         tvdb_match = self.lookup_tvdb(searchtitle, channel, manual_select=manual_select)
  129.                         log_msg("pvrart lookup for title: %s - TVDB result: %s" % (searchtitle, tvdb_match))
  130.                         if tvdb_match:
  131.                             # get full tvdb results and extend with tmdb
  132.                             if not details["media_type"]:
  133.                                 details["media_type"] = "tvshow"
  134.                             details = extend_dict(details, self._mutils.thetvdb.get_series(tvdb_match))
  135.                             details = extend_dict(details, self._mutils.tmdb.get_videodetails_by_externalid(
  136.                                 tvdb_match, "tvdb_id"), ["poster", "fanart"])
  137.  
  138.                     # fanart.tv scraping - append result to existing art
  139.                     if details.get("imdbnumber") and details["media_type"] == "movie":
  140.                         details["art"] = extend_dict(
  141.                             details["art"], self._mutils.fanarttv.movie(
  142.                                 details["imdbnumber"]), [
  143.                                 "poster", "fanart", "landscape"])
  144.                     elif details.get("tvdb_id") and details["media_type"] == "tvshow":
  145.                         details["art"] = extend_dict(
  146.                             details["art"], self._mutils.fanarttv.tvshow(
  147.                                 details["tvdb_id"]), [
  148.                                 "poster", "fanart", "landscape"])
  149.  
  150.                     # append omdb details
  151.                     if details.get("imdbnumber"):
  152.                         details = extend_dict(
  153.                             details, self._mutils.omdb.get_details_by_imdbid(
  154.                                 details["imdbnumber"]), [
  155.                                 "rating", "votes"])
  156.  
  157.                     # set thumbnail - prefer scrapers
  158.                     thumb = ""
  159.                     if details.get("thumbnail"):
  160.                         thumb = details["thumbnail"]
  161.                     elif details["art"].get("landscape"):
  162.                         thumb = details["art"]["landscape"]
  163.                     elif details["art"].get("fanart"):
  164.                         thumb = details["art"]["fanart"]
  165.                     elif details["art"].get("poster"):
  166.                         thumb = details["art"]["poster"]
  167.                     # use google images as last-resort fallback for thumbs - if enabled
  168.                     elif self._mutils.addon.getSetting("pvr_art_google") == "true":
  169.                         if manual_select:
  170.                             google_title = searchtitle
  171.                         else:
  172.                             google_title = '%s %s' % (searchtitle, channel.lower().split(" hd")[0])
  173.                         thumb = self._mutils.google.search_image(google_title, manual_select)
  174.                     if thumb:
  175.                         details["thumbnail"] = thumb
  176.                         details["art"]["thumb"] = thumb
  177.                     # extrafanart
  178.                     if details["art"].get("fanarts"):
  179.                         for count, item in enumerate(details["art"]["fanarts"]):
  180.                             details["art"]["fanart.%s" % count] = item
  181.                         if not details["art"].get("extrafanart") and len(details["art"]["fanarts"]) > 1:
  182.                             details["art"]["extrafanart"] = "plugin://script.skin.helper.service/"\
  183.                                 "?action=extrafanart&fanarts=%s" % quote_plus(repr(details["art"]["fanarts"]))
  184.  
  185.                     # download artwork to custom folder
  186.                     if self._mutils.addon.getSetting("pvr_art_download") == "true":
  187.                         details["art"] = download_artwork(self.get_custom_path(searchtitle, title), details["art"])
  188.  
  189.             log_msg("pvrart lookup for title: %s - final result: %s" % (searchtitle, details))
  190.  
  191.         # always store result in cache
  192.         # manual lookups should not expire too often
  193.         if manual_select:
  194.             self._mutils.cache.set(cache_str, details, expiration=timedelta(days=365))
  195.         else:
  196.             self._mutils.cache.set(cache_str, details)
  197.         return details
  198.  
  199.     def manual_set_pvr_artwork(self, title, channel, genre):
  200.         """manual override artwork options"""
  201.  
  202.         details = self.get_pvr_artwork(title, channel, genre)
  203.         cache_str = details["cachestr"]
  204.  
  205.         # show dialogselect with all artwork options
  206.         from utils import manual_set_artwork
  207.         changemade, artwork = manual_set_artwork(details["art"], "pvr")
  208.         if changemade:
  209.             details["art"] = artwork
  210.             # save results in cache
  211.             self._mutils.cache.set(cache_str, details, expiration=timedelta(days=365))
  212.  
  213.     def pvr_artwork_options(self, title, channel, genre):
  214.         """show options for pvr artwork"""
  215.         if not channel and genre:
  216.             channel, genre = self.get_pvr_channel_and_genre(title)
  217.         ignorechannels = self._mutils.addon.getSetting("pvr_art_ignore_channels").split("|")
  218.         ignoretitles = self._mutils.addon.getSetting("pvr_art_ignore_titles").split("|")
  219.         options = []
  220.         options.append(self._mutils.addon.getLocalizedString(32028))  # Refresh item (auto lookup)
  221.         options.append(self._mutils.addon.getLocalizedString(32029))  # Refresh item (manual lookup)
  222.         options.append(self._mutils.addon.getLocalizedString(32036))  # Choose art
  223.         if channel in ignorechannels:
  224.             options.append(self._mutils.addon.getLocalizedString(32030))  # Remove channel from ignore list
  225.         else:
  226.             options.append(self._mutils.addon.getLocalizedString(32031))  # Add channel to ignore list
  227.         if title in ignoretitles:
  228.             options.append(self._mutils.addon.getLocalizedString(32032))  # Remove title from ignore list
  229.         else:
  230.             options.append(self._mutils.addon.getLocalizedString(32033))  # Add title to ignore list
  231.         options.append(self._mutils.addon.getLocalizedString(32034))  # Open addon settings
  232.         header = self._mutils.addon.getLocalizedString(32035)
  233.         dialog = xbmcgui.Dialog()
  234.         ret = dialog.select(header, options)
  235.         del dialog
  236.         if ret == 0:
  237.             # Refresh item (auto lookup)
  238.             self.get_pvr_artwork(title=title, channel=channel, genre=genre, ignore_cache=True, manual_select=False)
  239.         elif ret == 1:
  240.             # Refresh item (manual lookup)
  241.             self.get_pvr_artwork(title=title, channel=channel, genre=genre, ignore_cache=True, manual_select=True)
  242.         elif ret == 2:
  243.             # Choose art
  244.             self.manual_set_pvr_artwork(title, channel, genre)
  245.         elif ret == 3:
  246.             # Add/remove channel to ignore list
  247.             if channel in ignorechannels:
  248.                 ignorechannels.remove(channel)
  249.             else:
  250.                 ignorechannels.append(channel)
  251.             ignorechannels_str = "|".join(ignorechannels)
  252.             self._mutils.addon.setSetting("pvr_art_ignore_channels", ignorechannels_str)
  253.             self.get_pvr_artwork(title=title, channel=channel, genre=genre, ignore_cache=True, manual_select=False)
  254.         elif ret == 4:
  255.             # Add/remove title to ignore list
  256.             if title in ignoretitles:
  257.                 ignoretitles.remove(title)
  258.             else:
  259.                 ignoretitles.append(title)
  260.             ignoretitles_str = "|".join(ignoretitles)
  261.             self._mutils.addon.setSetting("pvr_art_ignore_titles", ignoretitles_str)
  262.             self.get_pvr_artwork(title=title, channel=channel, genre=genre, ignore_cache=True, manual_select=False)
  263.         elif ret == 5:
  264.             # Open addon settings
  265.             xbmc.executebuiltin("Addon.OpenSettings(%s)" % ADDON_ID)
  266.  
  267.     def pvr_proceed_lookup(self, title, channel, genre, recordingdetails):
  268.         """perform some checks if we can proceed with the lookup"""
  269.         filters = []
  270.         if not title:
  271.             filters.append("Title is empty")
  272.         for item in self._mutils.addon.getSetting("pvr_art_ignore_titles").split("|"):
  273.             if item and item.lower() == title.lower():
  274.                 filters.append("Title is in list of titles to ignore")
  275.         for item in self._mutils.addon.getSetting("pvr_art_ignore_channels").split("|"):
  276.             if item and item.lower() == channel.lower():
  277.                 filters.append("Channel is in list of channels to ignore")
  278.         for item in self._mutils.addon.getSetting("pvr_art_ignore_genres").split("|"):
  279.             if genre and item and item.lower() in genre.lower():
  280.                 filters.append("Genre is in list of genres to ignore")
  281.         if self._mutils.addon.getSetting("pvr_art_ignore_commongenre") == "true":
  282.             # skip common genres like sports, weather, news etc.
  283.             genre = genre.lower()
  284.             kodi_strings = [19516, 19517, 19518, 19520, 19548, 19549, 19551,
  285.                             19552, 19553, 19554, 19555, 19556, 19557, 19558, 19559]
  286.             for kodi_string in kodi_strings:
  287.                 kodi_string = xbmc.getLocalizedString(kodi_string).lower()
  288.                 if (genre and (genre in kodi_string or kodi_string in genre)) or kodi_string in title:
  289.                     filters.append("Common genres like weather/sports are set to be ignored")
  290.         if self._mutils.addon.getSetting("pvr_art_recordings_only") == "true" and not recordingdetails:
  291.             filters.append("PVR Artwork is enabled for recordings only")
  292.         if filters:
  293.             filterstr = " - ".join(filters)
  294.             log_msg("PVR artwork - filter active for title: %s - channel %s --> %s" % (title, channel, filterstr))
  295.             return filterstr
  296.         else:
  297.             return ""
  298.  
  299.     @staticmethod
  300.     def get_mediatype_from_genre(genre):
  301.         """guess media type from genre for better matching"""
  302.         media_type = ""
  303.         if "movie" in genre.lower() or "film" in genre.lower():
  304.             media_type = "movie"
  305.         if "show" in genre.lower():
  306.             media_type = "tvshow"
  307.         if not media_type:
  308.             # Kodi defined movie genres
  309.             kodi_genres = [19500, 19507, 19508, 19602, 19603]
  310.             for kodi_genre in kodi_genres:
  311.                 if xbmc.getLocalizedString(kodi_genre) in genre:
  312.                     media_type = "movie"
  313.                     break
  314.         if not media_type:
  315.             # Kodi defined tvshow genres
  316.             kodi_genres = [19505, 19516, 19517, 19518, 19520, 19532, 19533, 19534, 19535, 19548, 19549,
  317.                            19550, 19551, 19552, 19553, 19554, 19555, 19556, 19557, 19558, 19559]
  318.             for kodi_genre in kodi_genres:
  319.                 if xbmc.getLocalizedString(kodi_genre) in genre:
  320.                     media_type = "tvshow"
  321.                     break
  322.         return media_type
  323.  
  324.     def get_searchtitle(self, title, channel):
  325.         """common logic to get a proper searchtitle from crappy titles provided by pvr"""
  326.         if not isinstance(title, unicode):
  327.             title = title.decode("utf-8")
  328.         title = title.lower()
  329.         # split characters - split on common splitters
  330.         splitters = self._mutils.addon.getSetting("pvr_art_splittitlechar").decode("utf-8").split("|")
  331.         if channel:
  332.             splitters.append(" %s" % channel.lower())
  333.         for splitchar in splitters:
  334.             title = title.split(splitchar)[0]
  335.         # replace common chars and words
  336.         title = re.sub(self._mutils.addon.getSetting("pvr_art_replace_by_space").decode("utf-8"), ' ', title)
  337.         title = re.sub(self._mutils.addon.getSetting("pvr_art_stripchars").decode("utf-8"), '', title)
  338.         title = title.strip()
  339.         return title
  340.  
  341.     def lookup_local_recording(self, title, channel):
  342.         """lookup actual recordings to get details for grouped recordings
  343.           also grab a thumb provided by the pvr
  344.        """
  345.         cache = self._mutils.cache.get("recordingdetails.%s%s" % (title, channel))
  346.         if cache:
  347.             return cache
  348.         details = {}
  349.         recordings = self._mutils.kodidb.recordings()
  350.         for item in recordings:
  351.             if (title == item["title"] or title in item["file"]) and (channel == item["channel"] or not channel):
  352.                 # grab thumb from pvr
  353.                 if item.get("art"):
  354.                     details["thumbnail"] = get_clean_image(item["art"].get("thumb"))
  355.                 # ignore tvheadend thumb as it returns the channellogo
  356.                 elif item.get("icon") and "imagecache" not in item["icon"]:
  357.                     details["thumbnail"] = get_clean_image(item["icon"])
  358.                 details["channel"] = item["channel"]
  359.                 details["genre"] = " / ".join(item["genre"])
  360.                 break
  361.         self._mutils.cache.set("recordingdetails.%s%s" % (title, channel), details)
  362.         return details
  363.  
  364.     def lookup_tvdb(self, searchtitle, channel, manual_select=False):
  365.         """helper to select a match on tvdb"""
  366.         tvdb_match = None
  367.         searchtitle = searchtitle.lower()
  368.         tvdb_result = self._mutils.thetvdb.search_series(searchtitle, True)
  369.         searchchannel = channel.lower().split("hd")[0].replace(" ", "")
  370.         match_results = []
  371.         if tvdb_result:
  372.             for item in tvdb_result:
  373.                 item["score"] = 0
  374.                 if not item["seriesName"]:
  375.                     continue  # seriesname can be None in some conditions
  376.                 itemtitle = item["seriesName"].lower()
  377.                 # high score if channel name matches
  378.                 # exact match on title - very high score
  379.                 if searchtitle == itemtitle:
  380.                     item["score"] += 1000
  381.                 # match title by replacing some characters
  382.                 if re.sub('\*|,|.\"|\'| |:|;', '', searchtitle) == re.sub('\*|,|.\"|\'| |:|;', '', itemtitle):
  383.                     item["score"] += 750
  384.                 # add SequenceMatcher score to the results
  385.                 stringmatchscore = SM(None, searchtitle, itemtitle).ratio()
  386.                 if stringmatchscore > 0.7:
  387.                     item["score"] += stringmatchscore * 500
  388.                 # prefer items with native language as we've searched with localized info enabled
  389.                 # prefer items with artwork
  390.                 if item["banner"]:
  391.                     item["score"] += 1
  392.                 if item["score"] > 500 or manual_select:
  393.                     match_results.append(item)
  394.             # sort our new list by score
  395.             match_results = sorted(match_results, key=itemgetter("score"), reverse=True)
  396.             if match_results and manual_select:
  397.                 # show selectdialog to manually select the item
  398.                 listitems = []
  399.                 for item in match_results:
  400.                     thumb = "http://thetvdb.com/banners/%s" % item["banner"] if item["banner"] else ""
  401.                     listitem = xbmcgui.ListItem(label=item["seriesName"], iconImage=thumb)
  402.                     listitems.append(listitem)
  403.                 dialog = DialogSelect(
  404.                     "DialogSelect.xml",
  405.                     "",
  406.                     listing=listitems,
  407.                     window_title="%s - TVDB" %
  408.                     xbmc.getLocalizedString(283))
  409.                 dialog.doModal()
  410.                 selected_item = dialog.result
  411.                 del dialog
  412.                 if selected_item != -1:
  413.                     tvdb_match = match_results[selected_item]["id"]
  414.                 else:
  415.                     match_results = []
  416.             if not tvdb_match and match_results:
  417.                 # just grab the first item as best match
  418.                 tvdb_match = match_results[0]["id"]
  419.         return tvdb_match
  420.  
  421.     def get_custom_path(self, searchtitle, title):
  422.         """locate custom folder on disk as pvrart location"""
  423.         title_path = ""
  424.         custom_path = self._mutils.addon.getSetting("pvr_art_custom_path")
  425.         if custom_path and self._mutils.addon.getSetting("pvr_art_custom") == "true":
  426.             delim = "\\" if "\\" in custom_path else "/"
  427.             dirs = xbmcvfs.listdir(custom_path)[0]
  428.             for strictness in [1, 0.95, 0.9, 0.8]:
  429.                 if title_path:
  430.                     break
  431.                 for directory in dirs:
  432.                     if title_path:
  433.                         break
  434.                     directory = directory.decode("utf-8")
  435.                     curpath = os.path.join(custom_path, directory) + delim
  436.                     for item in [title, searchtitle]:
  437.                         match = SM(None, item, directory).ratio()
  438.                         if match >= strictness:
  439.                             title_path = curpath
  440.                             break
  441.             if not title_path and self._mutils.addon.getSetting("pvr_art_download") == "true":
  442.                 title_path = os.path.join(custom_path, normalize_string(title)) + delim
  443.         return title_path
  444.  
  445.     def lookup_custom_path(self, searchtitle, title):
  446.         """looks up a custom directory if it contains a subdir for our title"""
  447.         details = {}
  448.         details["art"] = {}
  449.         title_path = self.get_custom_path(searchtitle, title)
  450.         if title_path and xbmcvfs.exists(title_path):
  451.             # we have found a folder for the title, look for artwork
  452.             files = xbmcvfs.listdir(title_path)[1]
  453.             for item in files:
  454.                 item = item.decode("utf-8")
  455.                 if item in ["banner.jpg", "clearart.png", "poster.jpg", "disc.png", "characterart.png",
  456.                             "fanart.jpg", "landscape.jpg"]:
  457.                     key = item.split(".")[0]
  458.                     details["art"][key] = title_path + item
  459.                 elif item == "logo.png":
  460.                     details["art"]["clearlogo"] = title_path + item
  461.                 elif item == "thumb.jpg":
  462.                     details["art"]["thumb"] = title_path + item
  463.             # extrafanarts
  464.             efa_path = title_path + "extrafanart/"
  465.             if xbmcvfs.exists(title_path + "extrafanart"):
  466.                 files = xbmcvfs.listdir(efa_path)[1]
  467.                 details["art"]["fanarts"] = []
  468.                 if files:
  469.                     details["art"]["extrafanart"] = efa_path
  470.                     for item in files:
  471.                         item = efa_path + item.decode("utf-8")
  472.                         details["art"]["fanarts"].append(item)
  473.         return details
  474.  
  475.     def lookup_local_library(self, title, media_type):
  476.         """lookup the title in the local video db"""
  477.         details = {}
  478.         filters = [{"operator": "is", "field": "title", "value": title}]
  479.         if not media_type or media_type == "tvshow":
  480.             kodi_items = self._mutils.kodidb.tvshows(filters=filters, limits=(0, 1))
  481.             if kodi_items:
  482.                 details = kodi_items[0]
  483.                 details["media_type"] = "tvshow"
  484.         if not details and (not media_type or media_type == "movie"):
  485.             kodi_items = self._mutils.kodidb.movies(filters=filters, limits=(0, 1))
  486.             if kodi_items:
  487.                 details = kodi_items[0]
  488.                 details["media_type"] = "movie"
  489.         if details:
  490.             for artkey, artvalue in details["art"].iteritems():
  491.                 details["art"][artkey] = get_clean_image(artvalue)
  492.             # todo: check extrafanart ?
  493.         return details
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement