# -*- coding: utf-8 -*- ################################################################################################# import json import logging import requests import os import shutil import sys from datetime import timedelta import xbmc import xbmcgui import xbmcplugin import xbmcaddon import xbmcvfs 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 from dialogs import resume from utils import window, settings, language as lang, JSONRPC ################################################################################################# log = logging.getLogger("EMBY."+__name__) KODI_V = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) ################################################################################################# class PlaybackUtils(object): def __init__(self, item=None, item_id=None): self.artwork = artwork.Artwork() self.emby = embyserver.Read_EmbyServer() self.item = item or self.emby.getItem(item_id) self.API = api.API(self.item) self.server = window('emby_server%s' % window('emby_currUser')) self.stack = [] if self.item['Type'] == "Audio": self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) else: self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) def _detect_widgets(self): kodi_version = xbmc.getInfoLabel('System.BuildVersion') if KODI_V == 18: return False elif kodi_version and "Git:" in kodi_version and kodi_version.split('Git:')[1].split("-")[0] in ('20171119', 'a9a7a20'): #TODO: To be reviewed once Leia is out. log.info("Build does not require workaround for widgets?") return False ''' 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") ''' 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): listitem = xbmcgui.ListItem() log.info("Play called %s: %s", item_id, self.item['Name']) resume = window('emby.resume') window('emby.resume', clear=True) play_url = putils.PlayUtils(self.item, listitem).get_play_url(force_transcode) 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) 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 if force_transcode: log.info("Clear the playlist.") self.playlist.clear() self.set_playlist(play_url, item_id, listitem, seektime, dbid) ##### SETUP PLAYBACK ''' To get everything to work together, play the first item in the stack with setResolvedUrl, add the rest to the regular playlist. ''' index = max(self.playlist.getposition(), 0) + 1 # Can return -1 force_play = False ''' Krypton 17.6 broke StartOffset. Seems to be working in Leia. For now, set up using StartPercent and adjust a bit to compensate. TODO: Once Leia is fully supported, move back to StartOffset. ''' 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)) # 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]) try: if self._detect_widgets(): # widgets do not fill artwork correctly log.info("Detected widget.") raise IndexError 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 for stack in self.stack: self.playlist.add(url=stack[0], listitem=stack[1], index=index) index += 1 if force_play: if len(sys.argv): xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, self.stack[0][1]) xbmc.Player().play(self.playlist, windowed=False) @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] def set_playlist(self, play_url, item_id, listitem, seektime=None, db_id=None): ##### CHECK FOR INTROS if settings('enableCinema') == "true" and not seektime: self._set_intros(item_id) ##### ADD MAIN ITEM self.set_properties(play_url, listitem) self.set_listitem(listitem, db_id) self.stack.append([play_url, listitem]) ##### ADD ADDITIONAL PARTS if self.item.get('PartCount'): self._set_additional_parts(item_id) 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) if intros['Items']: enabled = True if settings('askCinema') == "true": resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016)) if not resp: # User selected to not play trailers enabled = False log.info("Skip trailers.") if enabled: for intro in intros['Items']: listitem = xbmcgui.ListItem() url = putils.PlayUtils(intro, listitem).get_play_url() log.info("Adding Intro: %s", url) PlaybackUtils(intro).set_properties(url, listitem) self.set_artwork(listitem, self.item['Type']) self.set_listitem(listitem) self.stack.append([url, listitem]) window('emby.skip.%s' % self.item['Id'], value="true") def _set_additional_parts(self, item_id): parts = self.emby.get_additional_parts(item_id) for part in parts['Items']: listitem = xbmcgui.ListItem() url = putils.PlayUtils(part, listitem).get_play_url() log.info("Adding additional part: %s", url) # Set listitem and properties for each additional parts pb = PlaybackUtils(part) pb.set_properties(url, listitem) self.stack.append([url, listitem]) def set_listitem(self, listitem, dbid=None): people = self.API.get_people() mediatype = self.item['Type'] metadata = { 'title': self.item.get('Name', "Missing name"), 'year': self.item.get('ProductionYear'), 'plot': self.API.get_overview(), 'director': people.get('Director'), 'writer': people.get('Writer'), 'mpaa': self.API.get_mpaa(), 'genre': " / ".join(self.item['Genres']), 'studio': " / ".join(self.API.get_studios()), 'aired': self.API.get_premiere_date(), 'rating': self.item.get('CommunityRating'), 'votes': self.item.get('VoteCount'), 'dbid': dbid or None } if mediatype == "Episode": # Only for tv shows 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" elif mediatype == "MusicVideo": metadata['mediatype'] = "musicvideo" elif mediatype == "Audio": metadata['mediatype'] = "song" 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": 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", 'thumb': "Primary" } else: art = { 'poster': "Primary", 'clearart': "Art", 'clearlogo': "Logo", 'discart': "Disc", 'fanart_image': "Backdrop", 'landscape': "Thumb", 'thumb': "Primary" } for k_art, e_art in art.items(): if e_art == "Backdrop": self._set_art(listitem, k_art, all_artwork[e_art][0] if all_artwork[e_art] else " ") else: self._set_art(listitem, k_art, all_artwork.get(e_art, " ")) def _set_art(self, listitem, art, path): 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}) 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 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) if item_ids.index(item_id) == 0 and seektime: seektime = seektime / 10000000.0 if seektime else None log.info("Seektime detected: %s", self.API.adjust_resume(seektime)) listitem.setProperty('startoffset', str(self.API.adjust_resume(seektime))) 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: 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) return True