diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 46f3bd0e..05cedd70 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -418,6 +418,14 @@ msgctxt "#33009" msgid "Invalid username or password" msgstr "" +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "" + msgctxt "#33015" msgid "Delete file from Emby?" msgstr "" @@ -757,3 +765,15 @@ msgstr "" msgctxt "#33156" msgid "A patch has been applied!" msgstr "" + +msgctxt "#33157" +msgid "Audio only" +msgstr "" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "" diff --git a/resources/lib/emby/core/api.py b/resources/lib/emby/core/api.py index 8c5159d4..f58d2929 100644 --- a/resources/lib/emby/core/api.py +++ b/resources/lib/emby/core/api.py @@ -172,6 +172,9 @@ def delete_item(item_id): def get_local_trailers(item_id): return user_items("/%s/LocalTrailers" % item_id) +def get_transcode_settings(): + return _get('System/Configuration/encoding') + def get_ancestors(item_id): return items("/%s/Ancestors" % item_id, params={ 'UserId': "{UserId}" diff --git a/resources/lib/entrypoint/default.py b/resources/lib/entrypoint/default.py index fb70a866..fc6d95ce 100644 --- a/resources/lib/entrypoint/default.py +++ b/resources/lib/entrypoint/default.py @@ -69,7 +69,7 @@ class Events(object): elif mode =='play': item = TheVoid('GetItem', {'Id': params['id'], 'ServerId': server}).get() - Actions(params.get('server')).play(item, params.get('dbid')) + Actions(params.get('server')).play(item, params.get('dbid'), playlist=params.get('playlist') == 'true') elif mode == 'playlist': event('PlayPlaylist', {'Id': params['id'], 'ServerId': server}) diff --git a/resources/lib/helper/playutils.py b/resources/lib/helper/playutils.py index 5564adaa..0c1d371b 100644 --- a/resources/lib/helper/playutils.py +++ b/resources/lib/helper/playutils.py @@ -13,6 +13,7 @@ import xbmcvfs import api import database import client +import collections from . import _, settings, window, dialog from libraries import requests from downloader import TheVoid @@ -102,7 +103,7 @@ class PlayUtils(object): return sources - def select_source(self, sources): + def select_source(self, sources, audio=None, subtitle=None): if len(sources) > 1: selection = [] @@ -119,7 +120,7 @@ class PlayUtils(object): else: source = sources[0] - self.get(source) + self.get(source, audio, subtitle) return source @@ -171,7 +172,7 @@ class PlayUtils(object): return False - def get(self, source): + def get(self, source, audio=None, subtitle=None): ''' The server returns sources based on the MaxStreamingBitrate value and other filters. ''' @@ -192,10 +193,10 @@ class PlayUtils(object): else: LOG.info("--[ transcode ]") - self.transcode(source) + self.transcode(source, audio, subtitle) - self.info['AudioStreamIndex'] = source.get('DefaultAudioStreamIndex') - self.info['SubtitleStreamIndex'] = source.get('DefaultSubtitleStreamIndex') + self.info['AudioStreamIndex'] = self.info.get('AudioStreamIndex') or source.get('DefaultAudioStreamIndex') + self.info['SubtitleStreamIndex'] = self.info.get('SubtitleStreamIndex') or source.get('DefaultSubtitleStreamIndex') self.item['PlaybackInfo'].update(self.info) def live_stream(self, source): @@ -217,13 +218,23 @@ class PlayUtils(object): return info['MediaSource'] - def transcode(self, source): + def transcode(self, source, audio=None, subtitle=None): if not 'TranscodingUrl' in source: raise Exception("use get_sources to get transcoding url") self.info['Method'] = "Transcode" base, params = source['TranscodingUrl'].split('?') + + if settings('skipDialogTranscode') != "3" and source.get('MediaStreams'): + url_parsed = params.split('&') + + for i in url_parsed: + if 'AudioStreamIndex' in i or 'AudioBitrate' in i or 'SubtitleStreamIndex' in i: # handle manually + url_parsed.remove(i) + + params = "%s%s" % ('&'.join(url_parsed), self.get_audio_subs(source, audio, subtitle)) + self.info['Path'] = "%s/emby%s?%s" % (self.info['ServerAddress'], base.replace('stream', "master"), params) self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution()) @@ -422,8 +433,7 @@ class PlayUtils(object): if 'DeliveryUrl' in stream: url = "%s/emby%s" % (self.info['ServerAddress'], stream['DeliveryUrl']) else: - url = ("%s/emby/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % - (self.info['ServerAddress'], self.item['Id'], source['Id'], index, stream['Codec'], self.info['Token'])) + url = self.get_subtitles(source, stream, index) if url is None: continue @@ -462,7 +472,7 @@ class PlayUtils(object): path = os.path.join(temp, filename) try: - response = requests.get(src, stream=True) + response = requests.get(src, stream=True, verify=False) response.raise_for_status() except Exception as e: raise @@ -473,3 +483,113 @@ class PlayUtils(object): del response return path + + def get_audio_subs(self, source, audio=None, subtitle=None): + + ''' For transcoding only + Present the list of audio/subs to select from, before playback starts. + + Since Emby returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + ''' + prefs = "" + audio_streams = collections.OrderedDict() + subs_streams = collections.OrderedDict() + streams = source['MediaStreams'] + + for stream in streams: + + index = stream['Index'] + stream_type = stream['Type'] + + if stream_type == 'Audio': + + codec = stream['Codec'] + channel = stream.get('ChannelLayout', "") + + if 'Language' in stream: + track = "%s - %s - %s %s" % (index, stream['Language'], codec, channel) + else: + track = "%s - %s %s" % (index, codec, channel) + + audio_streams[track] = index + + elif stream_type == 'Subtitle': + + if 'Language' in stream: + track = "%s - %s" % (index, stream['Language']) + else: + track = "%s - %s" % (index, stream['Codec']) + + if stream['IsDefault']: + track = "%s - Default" % track + if stream['IsForced']: + track = "%s - Forced" % track + + subs_streams[track] = index + + skip_dialog = int(settings('skipDialogTranscode') or 0) + audio_selected = None + + if audio: + audio_selected = audio + + elif skip_dialog in (0, 1): + if len(audio_streams) > 1: + + selection = list(audio_streams.keys()) + resp = dialog("select", _(33013), selection) + audio_selected = audio_streams[selection[resp]] if resp else source['DefaultAudioStreamIndex'] + else: # Only one choice + audio_selected = audio_streams[next(iter(audio_streams))] + else: + audio_selected = source['DefaultAudioStreamIndex'] + + self.info['AudioStreamIndex'] = audio_selected + prefs += "&AudioStreamIndex=%s" % audio_selected + prefs += "&AudioBitrate=384000" if streams[audio_selected].get('Channels', 0) > 2 else "&AudioBitrate=192000" + + if subtitle: + + index = subtitle + server_settings = TheVoid('GetTranscodeOptions', {'ServerId': self.info['ServerId']}).get() + stream = streams[index] + + if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']: + self.info['SubtitleUrl'] = self.get_subtitles(source, stream, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + self.info['SubtitleStreamIndex'] = index + + elif skip_dialog in (0, 2) and len(subs_streams): + + selection = list(['No subtitles']) + list(subs_streams.keys()) + resp = dialog("select", _(33014), selection) + + if resp: + index = subs_streams[selection[resp]] if resp > -1 else source.get('DefaultSubtitleStreamIndex') + + if index is not None: + + server_settings = TheVoid('GetTranscodeOptions', {'ServerId': self.info['ServerId']}).get() + stream = streams[index] + + if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']: + self.info['SubtitleUrl'] = self.get_subtitles(source, stream, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + self.info['SubtitleStreamIndex'] = index + + return prefs + + def get_subtitles(self, source, stream, index): + + if 'DeliveryUrl' in stream: + url = "%s/emby%s" % (self.info['ServerAddress'], stream['DeliveryUrl']) + else: + url = ("%s/emby/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % + (self.info['ServerAddress'], self.item['Id'], source['Id'], index, stream['Codec'], self.info['Token'])) + + return url diff --git a/resources/lib/monitor.py b/resources/lib/monitor.py index 309896dd..0b4e248d 100644 --- a/resources/lib/monitor.py +++ b/resources/lib/monitor.py @@ -55,7 +55,7 @@ class Monitor(xbmc.Monitor): 'GetServerAddress', 'GetPlaybackInfo', 'Browse', 'GetImages', 'GetToken', 'PlayPlaylist', 'Play', 'GetIntros', 'GetAdditionalParts', 'RefreshItem', 'FavoriteItem', 'DeleteItem', 'AddUser', 'GetSession', 'GetUsers', 'GetThemes', - 'GetTheme', 'Playstate', 'GeneralCommand'): + 'GetTheme', 'Playstate', 'GeneralCommand', 'GetTranscodeOptions'): return data = json.loads(data)[0] @@ -139,6 +139,12 @@ class Monitor(xbmc.Monitor): window('emby_%s.json' % data['VoidName'], users) LOG.debug("--->[ beacon/emby_%s.json ] sent", data['VoidName']) + elif method == 'GetTranscodeOptions': + + result = server['api'].get_transcode_settings() + window('emby_%s.json' % data['VoidName'], result) + LOG.debug("--->[ beacon/emby_%s.json ] sent", data['VoidName']) + elif method == 'GetThemes': if data['Type'] == 'Video': @@ -183,8 +189,10 @@ class Monitor(xbmc.Monitor): elif method == 'Play': - items = server['api'].get_items(data['ItemIds']) - PlaylistWorker(data.get('ServerId'), items['Items'], data['PlayCommand'] == 'PlayNow', + item = server['api'].get_item(data['ItemIds'].pop(0)) + data['ItemIds'].insert(0, item) + + PlaylistWorker(data.get('ServerId'), data['ItemIds'], data['PlayCommand'] == 'PlayNow', data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), data.get('SubtitleStreamIndex')).start() diff --git a/resources/lib/objects/actions.py b/resources/lib/objects/actions.py index a681660f..6996a4c0 100644 --- a/resources/lib/objects/actions.py +++ b/resources/lib/objects/actions.py @@ -43,7 +43,7 @@ class Actions(object): return xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - def play(self, item, db_id=None, transcode=False): + def play(self, item, db_id=None, transcode=False, playlist=False): ''' Play item based on if playback started from widget ot not. To get everything to work together, play the first item in the stack with setResolvedUrl, @@ -63,7 +63,7 @@ class Actions(object): self.stack[0][1].setPath(self.stack[0][0]) try: - if self.detect_widgets(item): + if not playlist and self.detect_widgets(item): LOG.info(" [ play/widget ]") raise IndexError @@ -172,10 +172,10 @@ class Actions(object): def play_playlist(self, items, clear=True, seektime=None, audio=None, subtitle=None): - ''' Play a list of items. Creates a new playlist. + ''' Play a list of items. Creates a new playlist. Add additional items as plugin listing. ''' - playlist = self.get_playlist(items[0]) - started = False + item = items[0] + playlist = self.get_playlist(item) if clear: playlist.clear() @@ -183,32 +183,35 @@ class Actions(object): else: index = max(playlist.getposition(), 0) + 1 # Can return -1 - for order, item in enumerate(items): + listitem = xbmcgui.ListItem() + LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + play = playutils.PlayUtils(item, False, self.server_id, self.server) + source = play.select_source(play.get_sources()) + play.set_external_subs(source, listitem) + + item['PlaybackInfo']['AudioStreamIndex'] = audio or item['PlaybackInfo']['AudioStreamIndex'] + item['PlaybackInfo']['SubtitleStreamIndex'] = subtitle or item['PlaybackInfo'].get('SubtitleStreamIndex') + + self.set_listitem(item, listitem, None, True if seektime else False) + listitem.setPath(item['PlaybackInfo']['Path']) + playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) + + playlist.add(item['PlaybackInfo']['Path'], listitem, index) + index += 1 + + if clear: + xbmc.Player().play(playlist) + + for item in items[1:]: listitem = xbmcgui.ListItem() - LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + LOG.info("[ playlist/%s ]", item) + path = "plugin://plugin.video.emby/?mode=play&id=%s&playlist=true" % item - play = playutils.PlayUtils(item, False, self.server_id, self.server) - source = play.select_source(play.get_sources()) - play.set_external_subs(source, listitem) - - if order == 0: # First item - - item['PlaybackInfo']['AudioStreamIndex'] = audio or item['PlaybackInfo']['AudioStreamIndex'] - item['PlaybackInfo']['SubtitleStreamIndex'] = subtitle or item['PlaybackInfo'].get('SubtitleStreamIndex') - - self.set_listitem(item, listitem, None, True if order == 0 and seektime else False) - listitem.setPath(item['PlaybackInfo']['Path']) - playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) - - playlist.add(item['PlaybackInfo']['Path'], listitem, index) + listitem.setPath(path) + playlist.add(path, listitem, index) index += 1 - if not started and clear: - - started = True - xbmc.Player().play(playlist) - def set_listitem(self, item, listitem, db_id=None, seektime=None, intro=False): objects = Objects() @@ -237,8 +240,15 @@ class Actions(object): self.listitem_video(obj, listitem, item, seektime) - if 'PlaybackInfo' in item and seektime: - item['PlaybackInfo']['CurrentPosition'] = obj['Resume'] + if 'PlaybackInfo' in item: + + if seektime: + item['PlaybackInfo']['CurrentPosition'] = obj['Resume'] + + if 'SubtitleUrl' in item['PlaybackInfo']: + + LOG.info("[ subtitles ] %s", item['PlaybackInfo']['SubtitleUrl']) + listitem.setSubtitles([item['PlaybackInfo']['SubtitleUrl']]) listitem.setContentLookup(False) diff --git a/resources/lib/player.py b/resources/lib/player.py index 2516ab15..b7508904 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -254,9 +254,14 @@ class Player(xbmc.Player): ''' Report playback progress to emby server. Check if the user seek. ''' - current_file = self.getPlayingFile() + try: + current_file = self.getPlayingFile() + + if current_file not in self.played: + return + except Exception as error: + LOG.error(error) - if current_file not in self.played: return item = self.played[current_file] @@ -366,7 +371,7 @@ class Player(xbmc.Player): elif item['PlayMethod'] == 'Transcode': - LOG.info("Transcoding for %s terminated.", item['Id']) + LOG.info("<[ transcode/%s ]", item['Id']) item['Server']['api'].close_transcode(item['DeviceId']) diff --git a/resources/settings.xml b/resources/settings.xml index 6d60086d..3135493f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -41,7 +41,7 @@ - +