|
| 1 | +# Copyright (C) 2019, Roman Miroshnychenko aka Roman V.M. |
| 2 | +# |
| 3 | +# This program is free software: you can redistribute it and/or modify |
| 4 | +# it under the terms of the GNU General Public License as published by |
| 5 | +# the Free Software Foundation, either version 3 of the License, or |
| 6 | +# (at your option) any later version. |
| 7 | +# |
| 8 | +# This program is distributed in the hope that it will be useful, |
| 9 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +# GNU General Public License for more details. |
| 12 | +# |
| 13 | +# You should have received a copy of the GNU General Public License |
| 14 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 15 | + |
| 16 | +"""Plugin route actions""" |
| 17 | + |
| 18 | +import json |
| 19 | +import logging |
| 20 | +import sys |
| 21 | +from typing import Optional |
| 22 | +from urllib import parse as urllib_parse |
| 23 | + |
| 24 | +import xbmcgui |
| 25 | +import xbmcplugin |
| 26 | + |
| 27 | +from . import tvmaze_api, data_service |
| 28 | +from .kodi_utils import Settings |
| 29 | + |
| 30 | +HANDLE = int(sys.argv[1]) |
| 31 | + |
| 32 | + |
| 33 | +def find_show(title: str, year: Optional[str] = None) -> None: |
| 34 | + """Find a show by title""" |
| 35 | + search_results = data_service.search_show(title, year) |
| 36 | + for search_result in search_results: |
| 37 | + show_name = search_result['name'] |
| 38 | + if premiered := search_result.get('premiered'): |
| 39 | + show_name += f' ({premiered[:4]})' |
| 40 | + list_item = xbmcgui.ListItem(show_name, offscreen=True) |
| 41 | + data_service.add_basic_show_info(list_item, search_result) |
| 42 | + # Below "url" is some unique ID string (may be an actual URL to a show page) |
| 43 | + # that is used to get information about a specific TV show. |
| 44 | + xbmcplugin.addDirectoryItem( |
| 45 | + HANDLE, |
| 46 | + url=str(search_result['id']), |
| 47 | + listitem=list_item, |
| 48 | + isFolder=True |
| 49 | + ) |
| 50 | + |
| 51 | + |
| 52 | +def parse_nfo_file(nfo: str): |
| 53 | + """ |
| 54 | + Analyze NFO file contents |
| 55 | +
|
| 56 | + This function is called either instead of find_show |
| 57 | + if a tvshow.nfo file is found in the TV show folder or for each episode |
| 58 | + if episode NFOs are present along with episode files. |
| 59 | +
|
| 60 | + :param nfo: the contents of an NFO file |
| 61 | + """ |
| 62 | + is_tvshow_nfo = True |
| 63 | + logging.debug('Trying to parse NFO file:\n%s', nfo) |
| 64 | + tvmaze_id = None |
| 65 | + full_nfo = Settings().get_value_bool('full_nfo') |
| 66 | + if '<episodedetails>' in nfo: |
| 67 | + if full_nfo: |
| 68 | + return |
| 69 | + is_tvshow_nfo = False |
| 70 | + tvmaze_id = data_service.get_tvmaze_episode_id_from_xml_nfo(nfo) |
| 71 | + if tvmaze_id is None: |
| 72 | + # We cannot resolve an episode by alternative IDs or by title/year from TVmaze API |
| 73 | + return |
| 74 | + if tvmaze_id is None and '<tvshow>' in nfo: |
| 75 | + if full_nfo: |
| 76 | + return |
| 77 | + tvmaze_id = data_service.get_tvmaze_show_id_from_xml_nfo(nfo) |
| 78 | + if tvmaze_id is None: |
| 79 | + tvmaze_id = data_service.get_tvmaze_show_id_from_url_nfo(nfo) |
| 80 | + if tvmaze_id is None: |
| 81 | + return |
| 82 | + list_item = xbmcgui.ListItem(offscreen=True) |
| 83 | + id_string = str(tvmaze_id) |
| 84 | + uniqueids = {'tvmaze': id_string} |
| 85 | + info_tag = list_item.getVideoInfoTag() |
| 86 | + info_tag.setUniqueIDs(uniqueids, 'tvmaze') |
| 87 | + if is_tvshow_nfo: |
| 88 | + episodeguide = json.dumps(uniqueids) |
| 89 | + info_tag.setEpisodeGuide(episodeguide) |
| 90 | + # "url" is some string that uniquely identifies a show. |
| 91 | + # It may be an actual URL of a TV show page. |
| 92 | + xbmcplugin.addDirectoryItem( |
| 93 | + HANDLE, |
| 94 | + url=id_string, |
| 95 | + listitem=list_item, |
| 96 | + isFolder=True |
| 97 | + ) |
| 98 | + |
| 99 | + |
| 100 | +def get_details(show_id: Optional[str], unique_ids: Optional[str] = None) -> None: |
| 101 | + """Get details about a specific show""" |
| 102 | + logging.debug('Getting details for show id %s', show_id) |
| 103 | + if not show_id and unique_ids is not None: |
| 104 | + show_id = data_service.get_tvmaze_show_id_from_json_episodeguide(unique_ids) |
| 105 | + if not show_id: |
| 106 | + logging.error('Unable to determine TVmaze show ID. show_id: %s, unique_ids: %s', |
| 107 | + show_id, unique_ids) |
| 108 | + xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem(offscreen=True)) |
| 109 | + return |
| 110 | + show_info = tvmaze_api.load_show_info(show_id) |
| 111 | + if show_info is None: |
| 112 | + xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem(offscreen=True)) |
| 113 | + return |
| 114 | + list_item = xbmcgui.ListItem(show_info['name'], offscreen=True) |
| 115 | + data_service.add_full_show_info(list_item, show_info) |
| 116 | + xbmcplugin.setResolvedUrl(HANDLE, True, list_item) |
| 117 | + |
| 118 | + |
| 119 | +def get_episode_list(episodeguide: str, episode_order: str) -> None: # pylint: disable=missing-docstring |
| 120 | + logging.debug('Getting episode list for episodeguide %s, order: %s', |
| 121 | + episodeguide, episode_order) |
| 122 | + show_id = None |
| 123 | + if episodeguide.startswith('{'): |
| 124 | + show_id = data_service.get_tvmaze_show_id_from_json_episodeguide(episodeguide) |
| 125 | + if show_id is None: |
| 126 | + logging.error('Unable to determine TVmaze show ID from episodeguide: %s', episodeguide) |
| 127 | + return |
| 128 | + if show_id is None and not episodeguide.isdigit(): |
| 129 | + logging.warning('Invalid episodeguide format: %s (probably URL).', episodeguide) |
| 130 | + show_id = data_service.get_tvmaze_show_id_from_url_episodeguide(episodeguide) |
| 131 | + if show_id is None and episodeguide.isdigit(): |
| 132 | + logging.warning('Invalid episodeguide format: %s (a numeric string). ' |
| 133 | + 'Please consider re-scanning the show to update episodeguide record.', |
| 134 | + episodeguide) |
| 135 | + show_id = episodeguide |
| 136 | + if show_id is None: |
| 137 | + return |
| 138 | + episodes_map = data_service.get_episodes_map(show_id, episode_order) |
| 139 | + for episode in episodes_map.values(): |
| 140 | + list_item = xbmcgui.ListItem(episode['name'], offscreen=True) |
| 141 | + data_service.add_basic_episode_info(list_item, episode) |
| 142 | + encoded_ids = urllib_parse.urlencode({ |
| 143 | + 'show_id': show_id, |
| 144 | + 'episode_id': str(episode['id']), |
| 145 | + 'season': str(episode['season']), |
| 146 | + 'episode': str(episode['number']), |
| 147 | + }) |
| 148 | + # Below "url" is some unique ID string (it may be an actual URL to an episode page) |
| 149 | + # that allows to retrieve information about a specific episode. |
| 150 | + url = urllib_parse.quote(encoded_ids) |
| 151 | + xbmcplugin.addDirectoryItem( |
| 152 | + HANDLE, |
| 153 | + url=url, |
| 154 | + listitem=list_item, |
| 155 | + isFolder=True |
| 156 | + ) |
| 157 | + |
| 158 | + |
| 159 | +def get_episode_details(encoded_ids: str, episode_order: str) -> None: # pylint: disable=missing-docstring |
| 160 | + encoded_ids = urllib_parse.unquote(encoded_ids) |
| 161 | + decoded_ids = dict(urllib_parse.parse_qsl(encoded_ids)) |
| 162 | + logging.debug('Getting episode details for %s', decoded_ids) |
| 163 | + episode_info = data_service.get_episode_info(decoded_ids['show_id'], |
| 164 | + decoded_ids['episode_id'], |
| 165 | + decoded_ids['season'], |
| 166 | + decoded_ids['episode'], |
| 167 | + episode_order) |
| 168 | + if not episode_info: |
| 169 | + xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem(offscreen=True)) |
| 170 | + return |
| 171 | + list_item = xbmcgui.ListItem(episode_info['name'], offscreen=True) |
| 172 | + data_service.add_full_episode_info(list_item, episode_info) |
| 173 | + xbmcplugin.setResolvedUrl(HANDLE, True, list_item) |
| 174 | + |
| 175 | + |
| 176 | +def get_artwork(show_id: str) -> None: |
| 177 | + """ |
| 178 | + Get available artwork for a show |
| 179 | +
|
| 180 | + :param show_id: default unique ID set by setUniqueIDs() method |
| 181 | +
|
| 182 | + .. note:: This action does not seem to be used in Kodi v. 21 and above. |
| 183 | + You should set all shows artwork in the get_details action. |
| 184 | + """ |
| 185 | + logging.debug('Getting artwork for show ID %s', show_id) |
| 186 | + if show_id: |
| 187 | + show_info = tvmaze_api.load_show_info(show_id) |
| 188 | + if show_info is not None: |
| 189 | + list_item = xbmcgui.ListItem(show_info['name'], offscreen=True) |
| 190 | + data_service.set_show_artwork(show_info, list_item) |
| 191 | + xbmcplugin.setResolvedUrl(HANDLE, True, list_item) |
| 192 | + return |
| 193 | + xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem(offscreen=True)) |
| 194 | + |
| 195 | + |
| 196 | +def router(paramstring: str) -> None: |
| 197 | + """ |
| 198 | + Route addon calls |
| 199 | +
|
| 200 | + :param paramstring: url-encoded query string |
| 201 | + :raises RuntimeError: on unknown call action |
| 202 | + """ |
| 203 | + params = dict(urllib_parse.parse_qsl(paramstring)) |
| 204 | + logging.debug('Called addon with params: %s', str(sys.argv)) |
| 205 | + path_settings = json.loads(params.get('pathSettings') or '{}') |
| 206 | + logging.debug('Path settings: %s', path_settings) |
| 207 | + Settings().initialize(path_settings) |
| 208 | + episode_order = data_service.get_episode_order() |
| 209 | + if params['action'] == 'find': |
| 210 | + find_show(params['title'], params.get('year')) |
| 211 | + elif params['action'].lower() == 'nfourl': |
| 212 | + parse_nfo_file(params['nfo']) |
| 213 | + elif params['action'] == 'getdetails': |
| 214 | + url = params.get('url') |
| 215 | + unique_ids = params.get('uniqueIDs') |
| 216 | + get_details(url, unique_ids) |
| 217 | + elif params['action'] == 'getepisodelist': |
| 218 | + get_episode_list(params['url'], episode_order) |
| 219 | + elif params['action'] == 'getepisodedetails': |
| 220 | + get_episode_details(params['url'], episode_order) |
| 221 | + elif params['action'] == 'getartwork': |
| 222 | + get_artwork(params.get('id')) |
| 223 | + else: |
| 224 | + raise RuntimeError(f'Invalid addon call: {sys.argv}') |
| 225 | + xbmcplugin.endOfDirectory(HANDLE) |
0 commit comments