jellyfin-kodi/resources/lib/playbackutils.py

451 lines
16 KiB
Python
Raw Normal View History

2016-03-31 15:58:49 +00:00
# -*- coding: utf-8 -*-
#################################################################################################
import json
import logging
2016-10-28 05:02:47 +00:00
import requests
import os
import shutil
2016-03-31 15:58:49 +00:00
import sys
2018-03-21 05:27:11 +00:00
from datetime import timedelta
2016-03-31 15:58:49 +00:00
import xbmc
import xbmcgui
import xbmcplugin
2018-03-21 05:27:11 +00:00
import xbmcaddon
2016-10-28 05:02:47 +00:00
import xbmcvfs
2016-03-31 15:58:49 +00:00
import api
import artwork
import downloadutils
import playutils as putils
import playlist
import read_embyserver as embyserver
import shutil
import embydb_functions as embydb
from database import DatabaseConn
2018-03-21 05:27:11 +00:00
from dialogs import resume
2018-05-11 01:24:59 +00:00
from utils import window, settings, language as lang, JSONRPC
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
2018-03-21 05:27:11 +00:00
KODI_V = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
2016-03-31 15:58:49 +00:00
#################################################################################################
class PlaybackUtils(object):
2016-03-31 15:58:49 +00:00
def __init__(self, item=None, item_id=None):
2016-03-31 15:58:49 +00:00
self.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer()
self.item = item or self.emby.getItem(item_id)
self.API = api.API(self.item)
2016-03-31 15:58:49 +00:00
self.server = window('emby_server%s' % window('emby_currUser'))
2016-03-31 15:58:49 +00:00
self.stack = []
2016-03-31 15:58:49 +00:00
if self.item['Type'] == "Audio":
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
else:
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
2016-03-31 15:58:49 +00:00
2018-01-28 08:14:38 +00:00
def _detect_widgets(self):
kodi_version = xbmc.getInfoLabel('System.BuildVersion')
if KODI_V == 18:
return False
2018-04-15 07:28:12 +00:00
elif kodi_version and "Git:" in kodi_version and kodi_version.split('Git:')[1].split("-")[0] in ('20171119', 'a9a7a20'):
2018-03-05 07:15:08 +00:00
#TODO: To be reviewed once Leia is out.
log.info("Build does not require workaround for widgets?")
return False
2018-03-25 05:59:49 +00:00
'''
2018-03-21 04:25:09 +00:00
if not xbmc.getCondVisibility('Window.IsMedia'):
log.info("Not Window.IsMedia")
if self.item['Type'] == "Audio" and not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(music),1)'):
log.info("Audio and not playlist")
if not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(video),1)'):
log.info("Not video playlist")
'''
2018-01-28 08:14:38 +00:00
if (not xbmc.getCondVisibility('Window.IsMedia') and
((self.item['Type'] == "Audio" and not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(music),1)')) or
not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(video),1)'))):
return True
return False
def play(self, item_id, dbid=None, force_transcode=False):
2016-03-31 15:58:49 +00:00
listitem = xbmcgui.ListItem()
2016-10-28 05:02:47 +00:00
2018-05-11 01:24:59 +00:00
log.info("Play called %s: %s", item_id, self.item['Name'])
2016-03-31 15:58:49 +00:00
resume = window('emby.resume')
window('emby.resume', clear=True)
2016-03-31 15:58:49 +00:00
play_url = putils.PlayUtils(self.item, listitem).get_play_url(force_transcode)
2016-03-31 15:58:49 +00:00
if not play_url:
if play_url == False: # User backed-out of menu
self.playlist.clear()
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
2016-03-31 15:58:49 +00:00
2018-05-11 01:24:59 +00:00
if self.item['Type'] != "Audio":
''' Detect the seektime for video type content.
Verify the default video action set in Kodi for accurate resume behavior.
'''
if self.get_play_action() == "Resume":
resume = "true"
if force_transcode:
seektime = self.API.get_userdata()['Resume']
if seektime and resume != "true":
resume = self.resume_dialog(seektime)
if resume is None:
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
elif not resume:
seektime = 0
else:
seektime = self.API.adjust_resume(self.API.get_userdata()['Resume']) if resume == "true" else 0
2016-03-31 15:58:49 +00:00
if force_transcode:
log.info("Clear the playlist.")
self.playlist.clear()
2016-03-31 15:58:49 +00:00
2018-01-21 00:08:28 +00:00
self.set_playlist(play_url, item_id, listitem, seektime, dbid)
2016-03-31 15:58:49 +00:00
##### SETUP PLAYBACK
2016-03-31 15:58:49 +00:00
''' To get everything to work together, play the first item in the stack with setResolvedUrl,
add the rest to the regular playlist.
'''
2016-03-31 15:58:49 +00:00
index = max(self.playlist.getposition(), 0) + 1 # Can return -1
force_play = False
2016-03-31 15:58:49 +00:00
2018-01-27 09:16:24 +00:00
''' Krypton 17.6 broke StartOffset. Seems to be working in Leia.
2018-01-27 10:26:59 +00:00
For now, set up using StartPercent and adjust a bit to compensate.
2018-01-27 09:16:24 +00:00
TODO: Once Leia is fully supported, move back to StartOffset.
'''
2018-01-27 10:26:59 +00:00
if seektime:
seektime_percent = ((seektime/self.API.get_runtime()) * 100) - 0.40
log.info("seektime detected (percent): %s", seektime_percent)
listitem.setProperty('StartPercent', str(seektime_percent))
2018-01-27 09:16:24 +00:00
2018-03-05 07:15:08 +00:00
# Prevent manually mark as watched in Kodi monitor
window('emby.skip.%s' % item_id, value="true")
# Stack: [(url, listitem), (url, ...), ...]
self.stack[0][1].setPath(self.stack[0][0])
2018-04-03 23:38:53 +00:00
try:
2018-01-28 08:14:38 +00:00
if self._detect_widgets():
# widgets do not fill artwork correctly
log.info("Detected widget.")
raise IndexError
2016-03-31 15:58:49 +00:00
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1])
self.stack.pop(0) # remove the first item we just started.
except IndexError:
log.info("Playback activated via the context menu or widgets.")
force_play = True
2016-03-31 15:58:49 +00:00
for stack in self.stack:
self.playlist.add(url=stack[0], listitem=stack[1], index=index)
index += 1
2016-03-31 15:58:49 +00:00
if force_play:
2018-05-11 01:24:59 +00:00
if len(sys.argv) > 1:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, self.stack[0][1])
2018-03-27 10:36:46 +00:00
xbmc.Player().play(self.playlist, windowed=False)
2016-03-31 15:58:49 +00:00
2018-05-11 01:24:59 +00:00
@classmethod
def get_play_action(cls):
''' I could not figure out a way to listen to kodi setting changes?
For now, verify the play action every time play is called.
'''
options = ['Choose', 'Play', 'Resume', 'Show information']
result = JSONRPC('Settings.GetSettingValue').execute({'setting': "myvideos.selectaction"})
try:
return options[result['result']['value']]
except Exception as error:
log.error("Returning play action due to error: %s", error)
return options[1]
2018-03-25 05:59:49 +00:00
2018-01-21 00:08:28 +00:00
def set_playlist(self, play_url, item_id, listitem, seektime=None, db_id=None):
2016-03-31 15:58:49 +00:00
##### CHECK FOR INTROS
2016-03-31 15:58:49 +00:00
if settings('enableCinema') == "true" and not seektime:
self._set_intros(item_id)
2016-03-31 15:58:49 +00:00
##### ADD MAIN ITEM
2016-03-31 15:58:49 +00:00
self.set_properties(play_url, listitem)
self.set_listitem(listitem, db_id)
self.stack.append([play_url, listitem])
2016-03-31 15:58:49 +00:00
##### ADD ADDITIONAL PARTS
2016-03-31 15:58:49 +00:00
if self.item.get('PartCount'):
self._set_additional_parts(item_id)
2016-03-31 15:58:49 +00:00
def _set_intros(self, item_id):
# if we have any play them when the movie/show is not being resumed
intros = self.emby.get_intros(item_id)
2016-03-31 15:58:49 +00:00
if intros['Items']:
enabled = True
2016-03-31 15:58:49 +00:00
if settings('askCinema') == "true":
2016-03-31 15:58:49 +00:00
resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016))
if not resp:
# User selected to not play trailers
enabled = False
log.info("Skip trailers.")
2016-03-31 15:58:49 +00:00
if enabled:
for intro in intros['Items']:
2016-10-28 05:02:47 +00:00
listitem = xbmcgui.ListItem()
url = putils.PlayUtils(intro, listitem).get_play_url()
2018-01-23 23:43:26 +00:00
log.info("Adding Intro: %s", url)
2016-10-28 05:02:47 +00:00
PlaybackUtils(intro).set_properties(url, listitem)
self.set_artwork(listitem, self.item['Type'])
self.set_listitem(listitem)
2018-01-28 08:14:38 +00:00
self.stack.append([url, listitem])
2016-10-28 05:02:47 +00:00
window('emby.skip.%s' % self.item['Id'], value="true")
def _set_additional_parts(self, item_id):
2016-10-28 05:02:47 +00:00
parts = self.emby.get_additional_parts(item_id)
2016-10-28 05:02:47 +00:00
for part in parts['Items']:
2016-03-31 15:58:49 +00:00
listitem = xbmcgui.ListItem()
url = putils.PlayUtils(part, listitem).get_play_url()
2018-01-23 23:43:26 +00:00
log.info("Adding additional part: %s", url)
2016-03-31 15:58:49 +00:00
# Set listitem and properties for each additional parts
pb = PlaybackUtils(part)
pb.set_properties(url, listitem)
self.stack.append([url, listitem])
2016-03-31 15:58:49 +00:00
def set_listitem(self, listitem, dbid=None):
2016-03-31 15:58:49 +00:00
2016-10-28 05:02:47 +00:00
people = self.API.get_people()
mediatype = self.item['Type']
2016-03-31 15:58:49 +00:00
metadata = {
2016-03-31 16:30:52 +00:00
'title': self.item.get('Name', "Missing name"),
'year': self.item.get('ProductionYear'),
2016-10-28 05:02:47 +00:00
'plot': self.API.get_overview(),
2016-03-31 15:58:49 +00:00
'director': people.get('Director'),
'writer': people.get('Writer'),
2016-10-28 05:02:47 +00:00
'mpaa': self.API.get_mpaa(),
2016-03-31 16:30:52 +00:00
'genre': " / ".join(self.item['Genres']),
'studio': " / ".join(self.API.get_studios()),
2016-10-28 05:02:47 +00:00
'aired': self.API.get_premiere_date(),
2016-03-31 16:30:52 +00:00
'rating': self.item.get('CommunityRating'),
2018-03-10 10:29:21 +00:00
'votes': self.item.get('VoteCount'),
'dbid': dbid or None
2016-03-31 15:58:49 +00:00
}
if mediatype == "Episode":
2016-03-31 15:58:49 +00:00
# Only for tv shows
2016-10-28 05:02:47 +00:00
metadata['mediatype'] = "episode"
metadata['TVShowTitle'] = self.item.get('SeriesName', "")
metadata['season'] = self.item.get('ParentIndexNumber', -1)
metadata['episode'] = self.item.get('IndexNumber', -1)
elif mediatype == "Movie":
metadata['mediatype'] = "movie"
2016-10-28 05:02:47 +00:00
elif mediatype == "MusicVideo":
metadata['mediatype'] = "musicvideo"
2016-03-31 15:58:49 +00:00
elif mediatype == "Audio":
metadata['mediatype'] = "song"
2016-03-31 15:58:49 +00:00
else:
metadata['mediatype'] = "video"
listitem.setProperty('IsPlayable', 'true')
listitem.setProperty('IsFolder', 'false')
listitem.setLabel(metadata['title'])
listitem.setInfo('music' if mediatype == "Audio" else 'video', infoLabels=metadata)
def set_properties(self, url, listitem):
# Set all properties necessary for plugin path playback
item_id = self.item['Id']
item_type = self.item['Type']
info = window('emby_%s.play.json' % url)
window('emby_%s.play.json' % url, clear=True)
window('emby_%s.json' % url, {
'url': url,
'runtime': str(self.item.get('RunTimeTicks')),
'type': item_type,
'id': item_id,
'mediasource_id': info.get('mediasource_id', item_id),
'refreshid': self.item.get('SeriesId') if item_type == "Episode" else item_id,
'playmethod': info['playmethod'],
'playsession_id': info['playsession_id']
})
self.set_artwork(listitem, item_type)
listitem.setCast(self.API.get_actors())
def set_artwork(self, listitem, item_type):
all_artwork = self.artwork.get_all_artwork(self.item, parent_info=True)
# Set artwork for listitem
if item_type == "Episode":
2018-06-09 05:20:36 +00:00
art = {
'poster': "Series.Primary",
'tvshow.poster': "Series.Primary",
'clearart': "Art",
'tvshow.clearart': "Art",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Thumb",
'tvshow.landscape': "Thumb",
2018-06-09 05:20:36 +00:00
'thumb': "Primary",
'fanart': "Backdrop"
}
elif item_type in ("Artist", "Audio", "MusicAlbum"):
art = {
'clearlogo': "Logo",
'discart': "Disc",
'fanart': "Backdrop",
'fanart_image': "Backdrop", # in case
'thumb': "Primary"
}
else:
art = {
'poster': "Primary",
'clearart': "Art",
'clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Thumb",
2018-06-09 05:20:36 +00:00
'thumb': "Primary",
'fanart': "Backdrop"
}
for k_art, e_art in art.items():
2018-01-20 23:23:47 +00:00
if e_art == "Backdrop":
self._set_art(listitem, k_art, all_artwork[e_art][0] if all_artwork[e_art] else " ")
else:
2018-01-20 23:23:47 +00:00
self._set_art(listitem, k_art, all_artwork.get(e_art, " "))
def _set_art(self, listitem, art, path):
2018-01-20 23:23:47 +00:00
if art in ('fanart_image', 'small_poster', 'tiny_poster',
'medium_landscape', 'medium_poster', 'small_fanartimage',
'medium_fanartimage', 'fanart_noindicators'):
listitem.setProperty(art, path)
else:
listitem.setArt({art: path})
2018-03-21 05:27:11 +00:00
def resume_dialog(self, seektime):
log.info("Resume dialog called.")
XML_PATH = (xbmcaddon.Addon('plugin.video.emby').getAddonInfo('path'), "default", "1080i")
dialog = resume.ResumeDialog("script-emby-resume.xml", *XML_PATH)
dialog.set_resume_point("Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0])
dialog.doModal()
if dialog.is_selected():
if not dialog.get_selected(): # Start from beginning selected.
return False
else: # User backed out
log.info("User exited without a selection.")
return
return True
def play_all(self, item_ids, seektime=None, **kwargs):
self.playlist.clear()
started = False
2018-04-20 06:46:58 +00:00
index = max(self.playlist.getposition(), 0) + 1 # Can return -1
for item_id in item_ids:
listitem = xbmcgui.ListItem()
db_id = None
item = self.emby.getItem(item_id)
play_url = putils.PlayUtils(item, listitem, **kwargs if item_ids.index(item_id) == 0 else {}).get_play_url()
if not play_url:
log.info("Failed to retrieve playurl")
continue
log.info("Playurl: %s", play_url)
with DatabaseConn('emby') as cursor:
item_db = embydb.Embydb_Functions(cursor).getItem_byId(item_id)
db_id = item_db[0] if item_db else None
pbutils = PlaybackUtils(item)
2018-03-05 07:15:08 +00:00
if item_ids.index(item_id) == 0 and seektime:
2018-03-10 10:29:21 +00:00
seektime = seektime / 10000000.0 if seektime else None
log.info("Seektime detected: %s", self.API.adjust_resume(seektime))
2018-03-10 10:29:21 +00:00
listitem.setProperty('startoffset', str(self.API.adjust_resume(seektime)))
2018-03-10 10:29:21 +00:00
pbutils.set_playlist(play_url, item_id, listitem, seektime if item_ids.index(item_id) == 0 else None, db_id)
for stack in pbutils.stack:
2018-04-20 06:46:58 +00:00
self.playlist.add(url=stack[0], listitem=stack[1], index=index)
index += 1
if not started:
started = True
item = window('emby_%s.json' % play_url)
item['forcedaudio'] = kwargs.get('AudioStreamIndex')
item['forcedsubs'] = kwargs.get('SubtitleStreamIndex')
window('emby_%s.json' % play_url, value=item)
player = xbmc.Player()
player.play(pbutils.playlist)
2018-03-10 10:29:21 +00:00
return True