# -*- coding: utf-8 -*-
from __future__ import absolute_import
# Standard Library Imports
import logging
import inspect
# Kodi imports
import xbmcplugin
import xbmcgui
import xbmc
# Package imports
from codequick.script import Script
from codequick.support import build_path, logger_id
from codequick.utils import unicode_type, ensure_unicode
from codequick import localized
__all__ = ["Resolver"]
# Logger specific to this module
logger = logging.getLogger("%s.resolver" % logger_id)
[docs]class Resolver(Script):
"""
This class is used to create "Resolver" callbacks. Resolver callbacks are callbacks that
return playable video URL's which Kodi can play.
Resolver inherits all methods and attributes from :class:`script.Script<codequick.script.Script>`.
The possible return types from Resolver Callbacks are.
* ``str``: URL as type "str".
* ``iterable``: "List" or "tuple", consisting of URL's, "listItem's" or a "tuple" consisting of (title, URL).
* ``dict``: "Dictionary" consisting of "title" as the key and the URL as the value.
* ``listItem``: A :class:`codequick.Listitem<codequick.listing.Listitem>` object with required data already set e.g. "label" and "path".
* ``generator``: A Python "generator" that return's one or more URL's.
* ``False``: This will cause the "resolver call" to quit silently, without raising a RuntimeError.
.. note:: If multiple URL's are given, a playlist will be automaticly created.
:raises RuntimeError: If no content was returned from callback.
:raises ValueError: If returned url is invalid.
:example:
>>> from codequick import Resolver, Route, Listitem
>>>
>>> @Route.register
>>> def root(_):
>>> yield Listitem.from_dict("Play video", play_video,
>>> params={"url": "https://www.youtube.com/watch?v=RZuVTOk6ePM"})
>>>
>>> @Resolver.register
>>> def play_video(plugin, url):
>>> # Extract a playable video url using youtubeDL
>>> return plugin.extract_source(url)
"""
# Change listitem type to 'player'
is_playable = True
def __init__(self):
super(Resolver, self).__init__()
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self._extra_commands = {} # Extra options that are passed to listitem
def __call__(self, route, args, kwargs):
results = super(Resolver, self).__call__(route, args, kwargs)
return self._process_results(results)
def create_loopback(self, url, **next_params): # Undocumented
"""
Create a playlist where the second item loops back to add-on to load next video.
Also useful for continuous playback of videos with no foreseeable end. For example, party mode.
:param str url: URL of the first playable item.
:param next_params: [opt] "Keyword" arguments to add to the loopback request when accessing the next video.
:returns: The Listitem that Kodi will play.
:rtype: xbmcgui.ListItem
"""
# Video Playlist
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Main Playable listitem
main_listitem = xbmcgui.ListItem()
main_listitem.setPath(url)
# When called from a loopback we just add title to main listitem
if self._title.startswith(u"_loopback_"):
main_listitem.setLabel(self._title.split(u" - ", 1)[1])
next_params["_title_"] = self._title
else:
# Create playlist for loopback calling
# The first item is the playable listitem
main_listitem.setLabel(self._title)
next_params["_title_"] = u"_loopback_ - %s" % self._title
playlist.clear()
playlist.add(url, main_listitem)
# Create Loopback listitem
loop_listitem = xbmcgui.ListItem()
loop_listitem.setLabel(next_params["_title_"])
# Build a loopback url that callback to the addon to fetch the next video
loopback_url = build_path(**next_params)
loop_listitem.setPath(loopback_url)
playlist.add(loopback_url, loop_listitem)
# Retrun the playable listitem
return main_listitem
def _source_selection(self, video_info):
"""
Ask user whitch video stream to play.
:param video_info: YDStreamExtractor video_info object.
:returns: video_info object with the video pre selection.
"""
display_list = []
# Populate list with name of extractor ('YouTube') and video title.
for stream in video_info.streams():
data = "%s - %s" % (stream["ytdl_format"]["extractor"].title(), stream["title"])
display_list.append(data)
dialog = xbmcgui.Dialog()
ret = dialog.select(self.localize(localized.SELECT_PLAYBACK_ITEM), display_list)
if ret >= 0:
video_info.selectStream(ret)
return video_info
def _create_playlist(self, urls):
"""
Create playlist for kodi and return back the first item of that playlist to play.
:param list urls: Set of urls that will be used in the creation of the playlist.
List may consist of urls or listitem objects.
:returns The first listitem of the playlist.
:rtype: xbmcgui.ListItem
"""
# Loop each item to create playlist
listitems = [self._process_item(*item) for item in enumerate(urls, 1)]
# Populate Playlis
for item in listitems[1:]:
self.playlist.add(item.getPath(), item)
# Return the first playlist item
return listitems[0]
def _process_item(self, count, url):
"""
Process the playlist item and add to kodi playlist.
:param int count: The part number of the item
:param str url: The resolved object
"""
# Kodi original listitem object
if isinstance(url, xbmcgui.ListItem):
return url
# Custom listitem object
elif isinstance(url, Listitem):
# noinspection PyProtectedMember
return url.build()[1]
else:
# Not already a listitem object
listitem = xbmcgui.ListItem()
if isinstance(url, (list, tuple)):
title, url = url
title = ensure_unicode(title)
else:
title = self._title
# Create listitem with new title
listitem.setLabel(u"%s Part %i" % (title, count) if count > 1 else title)
listitem.setInfo("video", {"title": title})
listitem.setPath(url)
return listitem
def _process_generator(self, resolved):
"""
Populate the kodi playlist in the background from a generator.
:param resolved: The resolved generator to fetch the rest of the videos from
"""
for item in enumerate(filter(None, resolved), 2):
listitem = self._process_item(*item)
self.playlist.add(listitem.getPath(), listitem)
def _process_results(self, resolved):
"""
Construct playable listitem and send to kodi.
:param resolved: The resolved url to send back to kodi.
"""
if resolved:
# Create listitem object if resolved is a string or unicode
if isinstance(resolved, (bytes, unicode_type)):
listitem = xbmcgui.ListItem()
listitem.setPath(resolved)
# Directly use resoleved if its already a listitem
elif isinstance(resolved, xbmcgui.ListItem):
listitem = resolved
# Extract original kodi listitem from custom listitem
elif isinstance(resolved, Listitem):
# noinspection PyProtectedMember
listitem = resolved.build()[1]
# Create playlist if resolved object is a list of urls
elif isinstance(resolved, (list, tuple)):
listitem = self._create_playlist(resolved)
# Fetch the first element of the generator and process the rest in the background
elif inspect.isgenerator(resolved):
listitem = self._process_item(1, next(resolved))
self.register_delayed(self._process_generator, resolved)
# Create playlist if resolved is a dict of {title: url}
elif hasattr(resolved, "items"):
items = resolved.items()
listitem = self._create_playlist(items)
else:
# Resolved url must be invalid
raise ValueError("resolver returned invalid url of type: '%s'" % type(resolved))
logger.debug("Resolved Url: %s", listitem.getPath())
elif resolved is False:
# A empty listitem is still required even if 'resolved' is False
# From time to time Kodi will report that 'Playback failed'
# there is nothing that can be done about that.
listitem = xbmcgui.ListItem()
else:
raise RuntimeError(self.localize(localized.NO_VIDEO))
# Add extra parameters to listitem
if "setContentLookup" in self._extra_commands:
value = self._extra_commands["setContentLookup"]
listitem.setContentLookup(value)
# Send playable listitem to kodi
xbmcplugin.setResolvedUrl(self.handle, bool(resolved), listitem)
# Now we can import the listing module
from codequick.listing import Listitem