Files
jellycon/resources/lib/play_utils.py
2021-12-21 21:22:16 -05:00

1288 lines
46 KiB
Python

# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui
import xbmcaddon
from datetime import timedelta
import json
import os
import base64
from six.moves.urllib.parse import urlparse
from .loghandler import LazyLogger
from .downloadutils import DownloadUtils
from .resume_dialog import ResumeDialog
from .utils import PlayUtils, get_art, send_event_notification, convert_size
from .kodi_utils import HomeWindow
from .translation import string_load
from .datamanager import DataManager, clear_old_cache_data
from .item_functions import extract_item_info, add_gui_item
from .clientinfo import ClientInformation
from .cache_images import CacheArtwork
from .picture_viewer import PictureViewer
from .tracking import timer
from .playnext import PlayNextDialog
log = LazyLogger(__name__)
download_utils = DownloadUtils()
def play_all_files(items, play_items=True):
home_window = HomeWindow()
log.debug("playAllFiles called with items: {0}", items)
server = download_utils.get_server()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
playlist_data = {}
for item in items:
item_id = item.get("Id")
# get playback info
playback_info = download_utils.get_item_playback_info(item_id, False)
if playback_info is None:
log.debug("playback_info was None, could not get MediaSources so can not play!")
return
if playback_info.get("ErrorCode") is not None:
error_string = playback_info.get("ErrorCode")
xbmcgui.Dialog().notification(string_load(30316),
error_string,
icon="special://home/addons/plugin.video.jellycon/icon.png")
return
play_session_id = playback_info.get("PlaySessionId")
# select the media source to use
sources = playback_info.get('MediaSources')
selected_media_source = sources[0]
source_id = selected_media_source.get("Id")
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
if playurl is None:
return
playback_type_string = "DirectPlay"
if playback_type == "2":
playback_type_string = "Transcode"
elif playback_type == "1":
playback_type_string = "DirectStream"
# add the playback type into the overview
if item.get("Overview", None) is not None:
item["Overview"] = playback_type_string + "\n" + item.get("Overview")
else:
item["Overview"] = playback_type_string
# add title decoration is needed
item_title = item.get("Name", string_load(30280))
list_item = xbmcgui.ListItem(label=item_title)
# add playurl and data to the monitor
playlist_data[playurl] = {}
playlist_data[playurl]["item_id"] = item_id
playlist_data[playurl]["source_id"] = source_id
playlist_data[playurl]["playback_type"] = playback_type_string
playlist_data[playurl]["play_session_id"] = play_session_id
playlist_data[playurl]["play_action_type"] = "play_all"
home_window.set_property('playlist', json.dumps(playlist_data))
# Set now_playing to the first track
if len(playlist_data) == 1:
home_window.set_property('now_playing', json.dumps(playlist_data[playurl]))
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
playlist.add(playurl, list_item)
if play_items:
xbmc.Player().play(playlist)
return None
else:
return playlist
def play_list_of_items(id_list):
log.debug("Loading all items in the list")
data_manager = DataManager()
items = []
for item_id in id_list:
url = "{server}/Users/{userid}/Items/%s?format=json"
url = url % (item_id,)
result = data_manager.get_content(url)
if result is None:
log.debug("Playfile item was None, so can not play!")
return
items.append(result)
return play_all_files(items)
def add_to_playlist(play_info):
log.debug("Adding item to playlist : {0}".format(play_info))
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
server = download_utils.get_server()
item_id = play_info.get("item_id")
url = "{server}/Users/{userid}/Items/%s?format=json"
url = url % (item_id,)
data_manager = DataManager()
item = data_manager.get_content(url)
if item is None:
log.debug("Playfile item was None, so can not play!")
return
# get playback info
playback_info = download_utils.get_item_playback_info(item_id, False)
if playback_info is None:
log.debug("playback_info was None, could not get MediaSources so can not play!")
return
if playback_info.get("ErrorCode") is not None:
error_string = playback_info.get("ErrorCode")
xbmcgui.Dialog().notification(string_load(30316),
error_string,
icon="special://home/addons/plugin.video.jellycon/icon.png")
return
play_session_id = playback_info.get("PlaySessionId")
# select the media source to use
sources = playback_info.get('MediaSources')
selected_media_source = sources[0]
source_id = selected_media_source.get("Id")
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
if playurl is None:
return
playback_type_string = "DirectPlay"
if playback_type == "2":
playback_type_string = "Transcode"
elif playback_type == "1":
playback_type_string = "DirectStream"
# add the playback type into the overview
if item.get("Overview", None) is not None:
item["Overview"] = playback_type_string + "\n" + item.get("Overview")
else:
item["Overview"] = playback_type_string
# add title decoration is needed
item_title = item.get("Name", string_load(30280))
list_item = xbmcgui.ListItem(label=item_title)
# add playurl and data to the monitor
data = {}
data["item_id"] = item_id
data["source_id"] = source_id
data["playback_type"] = playback_type_string
data["play_session_id"] = play_session_id
data["play_action_type"] = "play_all"
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
playlist.add(playurl, list_item)
def get_playback_intros(item_id):
log.debug("get_playback_intros")
data_manager = DataManager()
url = "{server}/Users/{userid}/Items/%s/Intros" % item_id
intro_items = data_manager.get_content(url)
if intro_items is None:
log.debug("get_playback_intros failed!")
return
into_list = []
intro_items = intro_items["Items"]
for into in intro_items:
into_list.append(into)
return into_list
@timer
def play_file(play_info):
item_id = play_info.get("item_id")
home_window = HomeWindow()
last_url = home_window.get_property("last_content_url")
if last_url:
home_window.set_property("skip_cache_for_" + last_url, "true")
action = play_info.get("action", "play")
if action == "add_to_playlist":
add_to_playlist(play_info)
return
# if this is a list of items them add them all to the play list
if isinstance(item_id, list):
return play_list_of_items(item_id)
auto_resume = play_info.get("auto_resume", "-1")
force_transcode = play_info.get("force_transcode", False)
media_source_id = play_info.get("media_source_id", "")
subtitle_stream_index = play_info.get("subtitle_stream_index", None)
audio_stream_index = play_info.get("audio_stream_index", None)
log.debug("playFile id({0}) resume({1}) force_transcode({2})".format(item_id, auto_resume, force_transcode))
settings = xbmcaddon.Addon()
addon_path = settings.getAddonInfo('path')
force_auto_resume = settings.getSetting('forceAutoResume') == 'true'
jump_back_amount = int(settings.getSetting("jump_back_amount"))
play_cinema_intros = settings.getSetting('play_cinema_intros') == 'true'
server = download_utils.get_server()
url = "{server}/Users/{userid}/Items/%s?format=json" % (item_id,)
data_manager = DataManager()
result = data_manager.get_content(url)
log.debug("Playfile item: {0}".format(result))
if result is None:
log.debug("Playfile item was None, so can not play!")
return
# if this is a season, playlist or album then play all items in that parent
if result.get("Type") in ["Season", "MusicAlbum", "Playlist"]:
log.debug("PlayAllFiles for parent item id: {0}".format(item_id))
url = ('{server}/Users/{userid}/items' +
'?ParentId=%s' +
'&Fields=MediaSources' +
'&format=json')
url = url % (item_id,)
result = data_manager.get_content(url)
log.debug("PlayAllFiles items: {0}".format(result))
# process each item
items = result["Items"]
if items is None:
items = []
return play_all_files(items)
# if this is a program from live tv epg then play the actual channel
if result.get("Type") == "Program":
channel_id = result.get("ChannelId")
url = "{server}/Users/{userid}/Items/%s?format=json" % (channel_id,)
result = data_manager.get_content(url)
item_id = result["Id"]
if result.get("Type") == "Photo":
play_url = "%s/Items/%s/Images/Primary"
play_url = play_url % (server, item_id)
plugin_path = xbmc.translatePath(os.path.join(xbmcaddon.Addon().getAddonInfo('path')))
action_menu = PictureViewer("PictureViewer.xml", plugin_path, "default", "720p")
action_menu.setPicture(play_url)
action_menu.doModal()
return
# get playback info from the server using the device profile
playback_info = download_utils.get_item_playback_info(item_id, force_transcode)
if playback_info is None:
log.debug("playback_info was None, could not get MediaSources so can not play!")
return
if playback_info.get("ErrorCode") is not None:
error_string = playback_info.get("ErrorCode")
xbmcgui.Dialog().notification(string_load(30316),
error_string,
icon="special://home/addons/plugin.video.jellycon/icon.png")
return
play_session_id = playback_info.get("PlaySessionId")
# select the media source to use
media_sources = playback_info.get('MediaSources')
selected_media_source = None
if media_sources is None or len(media_sources) == 0:
log.debug("Play Failed! There is no MediaSources data!")
return
elif len(media_sources) == 1:
selected_media_source = media_sources[0]
elif media_source_id != "":
for source in media_sources:
if source.get("Id", "na") == media_source_id:
selected_media_source = source
break
elif len(media_sources) > 1:
items = []
for source in media_sources:
label = source.get("Name", "na")
label2 = __build_label2_from(source)
items.append(xbmcgui.ListItem(label=label, label2=label2))
dialog = xbmcgui.Dialog()
resp = dialog.select(string_load(30309), items, useDetails=True)
if resp > -1:
selected_media_source = media_sources[resp]
else:
log.debug("Play Aborted, user did not select a MediaSource")
return
if selected_media_source is None:
log.debug("Play Aborted, MediaSource was None")
return
source_id = selected_media_source.get("Id")
seek_time = 0
auto_resume = int(auto_resume)
# process user data for resume points
if auto_resume != -1:
seek_time = (auto_resume / 1000) / 10000
elif force_auto_resume:
user_data = result.get("UserData")
reasonable_ticks = int(user_data.get("PlaybackPositionTicks")) / 1000
seek_time = reasonable_ticks / 10000
else:
user_data = result.get("UserData")
if user_data.get("PlaybackPositionTicks") != 0:
reasonable_ticks = int(user_data.get("PlaybackPositionTicks")) / 1000
seek_time = reasonable_ticks / 10000
display_time = str(timedelta(seconds=seek_time))
resume_dialog = ResumeDialog("ResumeDialog.xml", addon_path, "default", "720p")
resume_dialog.setResumeTime("Resume from " + display_time)
resume_dialog.doModal()
resume_result = resume_dialog.getResumeAction()
del resume_dialog
log.debug("Resume Dialog Result: {0}".format(resume_result))
if resume_result == 1:
seek_time = 0
elif resume_result == -1:
return
log.debug("play_session_id: {0}".format(play_session_id))
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
log.info("Play URL: {0} Playback Type: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
if playurl is None:
return
playback_type_string = "DirectPlay"
if playback_type == "2":
playback_type_string = "Transcode"
elif playback_type == "1":
playback_type_string = "DirectStream"
# add the playback type into the overview
if result.get("Overview", None) is not None:
result["Overview"] = playback_type_string + "\n" + result.get("Overview")
else:
result["Overview"] = playback_type_string
# add title decoration is needed
item_title = result.get("Name", string_load(30280))
# extract item info from result
gui_options = {}
gui_options["server"] = server
gui_options["name_format"] = None
gui_options["name_format_type"] = ""
item_details = extract_item_info(result, gui_options)
# create ListItem
display_options = {}
display_options["addCounts"] = False
display_options["addResumePercent"] = False
display_options["addSubtitleAvailable"] = False
display_options["addUserRatings"] = False
gui_item = add_gui_item("", item_details, display_options, False)
list_item = gui_item[1]
if playback_type == "2": # if transcoding then prompt for audio and subtitle
playurl = audio_subs_pref(playurl, list_item, selected_media_source, item_id, audio_stream_index,
subtitle_stream_index)
log.debug("New playurl for transcoding: {0}".format(playurl))
elif playback_type == "1": # for direct stream add any streamable subtitles
external_subs(selected_media_source, list_item, item_id)
# add playurl and data to the monitor
data = {}
data["item_id"] = item_id
data["source_id"] = source_id
data["playback_type"] = playback_type_string
data["play_session_id"] = play_session_id
data["play_action_type"] = "play"
data["item_type"] = result.get("Type", None)
data["can_delete"] = result.get("CanDelete", False)
# Check for next episodes
if result.get('Type') == 'Episode':
next_episode = get_next_episode(result)
data["next_episode"] = next_episode
home_window.set_property('now_playing', json.dumps(data))
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, result, server, listitem_props, item_title)
player = xbmc.Player()
intro_items = []
if play_cinema_intros and seek_time == 0:
intro_items = get_playback_intros(item_id)
if len(intro_items) > 0:
playlist = play_all_files(intro_items, play_items=False)
playlist.add(playurl, list_item)
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
playlist.add(playurl, list_item)
player.play(playlist)
if seek_time != 0:
player.pause()
monitor = xbmc.Monitor()
count = 0
while not player.isPlaying() and not monitor.abortRequested() and count != 100:
count = count + 1
xbmc.sleep(100)
if count == 100 or not player.isPlaying() or monitor.abortRequested():
log.info("PlaybackResumrAction : Playback item did not get to a play state in 10 seconds so exiting")
player.stop()
return
log.info("PlaybackResumrAction : Playback is Running")
seek_to_time = seek_time - jump_back_amount
target_seek = (seek_to_time - 10)
count = 0
max_loops = 2 * 120
while not monitor.abortRequested() and player.isPlaying() and count < max_loops:
log.info("PlaybackResumrAction : Seeking to : {0}".format(seek_to_time))
player.seekTime(seek_to_time)
current_position = player.getTime()
if current_position >= target_seek:
break
log.info("PlaybackResumrAction : target:{0} current:{1}".format(target_seek, current_position))
count = count + 1
xbmc.sleep(500)
if count == max_loops:
log.info("PlaybackResumrAction : Playback could not seek to required position")
player.stop()
else:
count = 0
while bool(xbmc.getCondVisibility("Player.Paused")) and count < 10:
log.info("PlaybackResumrAction : Unpausing playback")
player.pause()
xbmc.sleep(1000)
count = count + 1
if count == 10:
log.info("PlaybackResumrAction : Could not unpause")
else:
log.info("PlaybackResumrAction : Playback resumed")
def __build_label2_from(source):
videos = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Video"]
audios = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Audio"]
subtitles = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Subtitle"]
details = [str(convert_size(source.get('Size', 0)))]
for video in videos:
details.append('{} {} {}bit'.format(video.get('DisplayTitle', ''),
video.get('VideoRange', ''),
video.get('BitDepth', '')))
aud = []
for audio in audios:
aud.append('{} {} {}'.format(audio.get('Language', ''),
audio.get('Codec', ''),
audio.get('Channels', '')))
if len(aud) > 0:
details.append(', '.join(aud).upper())
subs = []
for subtitle in subtitles:
subs.append(subtitle.get('Language', ''))
if len(subs) > 0:
details.append('S: {}'.format(', '.join(subs)).upper())
return ' | '.join(details)
def get_next_episode(item):
if item.get("Type") != "Episode":
log.debug("Not an episode, can not get next")
return None
parent_id = item.get("ParentId")
item_index = item.get("IndexNumber")
if parent_id is None:
log.debug("No parent id, can not get next")
return None
if item_index is None:
log.debug("No episode number, can not get next")
return None
url = ('{server}/Users/{userid}/Items?' +
'?Recursive=true' +
'&ParentId=' + parent_id +
'&IsVirtualUnaired=false' +
'&IsMissing=False' +
'&IncludeItemTypes=Episode' +
'&ImageTypeLimit=1' +
'&format=json')
data_manager = DataManager()
items_result = data_manager.get_content(url)
log.debug("get_next_episode, sibling list: {0}".format(items_result))
if items_result is None:
log.debug("get_next_episode no results")
return None
item_list = items_result.get("Items") or []
for item in item_list:
# find the very next episode in the season
if item.get("IndexNumber") == item_index + 1:
log.debug("get_next_episode, found next episode: {0}".format(item))
return item
return None
def send_next_episode_details(item, next_episode):
if next_episode is None:
log.debug("No next episode")
return
gui_options = {}
gui_options["server"] = download_utils.get_server()
gui_options["name_format"] = None
gui_options["name_format_type"] = ""
item_details = extract_item_info(item, gui_options)
next_item_details = extract_item_info(next_episode, gui_options)
current_item = {}
current_item["episodeid"] = item_details.id
current_item["tvshowid"] = item_details.series_name
current_item["title"] = item_details.name
current_item["art"] = {}
current_item["art"]["tvshow.poster"] = item_details.art.get('tvshow.poster', '')
current_item["art"]["thumb"] = item_details.art.get('thumb', '')
current_item["art"]["tvshow.fanart"] = item_details.art.get('tvshow.fanart', '')
current_item["art"]["tvshow.landscape"] = item_details.art.get('tvshow.landscape', '')
current_item["art"]["tvshow.clearart"] = item_details.art.get('tvshow.clearart', '')
current_item["art"]["tvshow.clearlogo"] = item_details.art.get('tvshow.clearlogo', '')
current_item["plot"] = item_details.plot
current_item["showtitle"] = item_details.series_name
current_item["playcount"] = item_details.play_count
current_item["season"] = item_details.season_number
current_item["episode"] = item_details.episode_number
current_item["rating"] = item_details.critic_rating
current_item["firstaired"] = item_details.year
next_item = {}
next_item["episodeid"] = next_item_details.id
next_item["tvshowid"] = next_item_details.series_name
next_item["title"] = next_item_details.name
next_item["art"] = {}
next_item["art"]["tvshow.poster"] = next_item_details.art.get('tvshow.poster', '')
next_item["art"]["thumb"] = next_item_details.art.get('thumb', '')
next_item["art"]["tvshow.fanart"] = next_item_details.art.get('tvshow.fanart', '')
next_item["art"]["tvshow.landscape"] = next_item_details.art.get('tvshow.landscape', '')
next_item["art"]["tvshow.clearart"] = next_item_details.art.get('tvshow.clearart', '')
next_item["art"]["tvshow.clearlogo"] = next_item_details.art.get('tvshow.clearlogo', '')
next_item["plot"] = next_item_details.plot
next_item["showtitle"] = next_item_details.series_name
next_item["playcount"] = next_item_details.play_count
next_item["season"] = next_item_details.season_number
next_item["episode"] = next_item_details.episode_number
next_item["rating"] = next_item_details.critic_rating
next_item["firstaired"] = next_item_details.year
next_info = {
"current_episode": current_item,
"next_episode": next_item,
"play_info": {
"item_id": next_item_details.id,
"auto_resume": False,
"force_transcode": False
}
}
send_event_notification("upnext_data", next_info, True)
def set_list_item_props(item_id, list_item, result, server, extra_props, title):
# set up item and item info
art = get_art(result, server=server)
list_item.setArt(art)
list_item.setProperty('IsPlayable', 'false')
list_item.setProperty('IsFolder', 'false')
list_item.setProperty('id', result.get("Id"))
for prop in extra_props:
list_item.setProperty(prop[0], prop[1])
item_type = result.get("Type", "").lower()
mediatype = 'video'
if item_type == 'movie' or item_type == 'boxset':
mediatype = 'movie'
elif item_type == 'series':
mediatype = 'tvshow'
elif item_type == 'season':
mediatype = 'season'
elif item_type == 'episode':
mediatype = 'episode'
elif item_type == 'audio':
mediatype = 'song'
if item_type == "audio":
details = {
'title': title,
'mediatype': mediatype
}
list_item.setInfo("Music", infoLabels=details)
else:
details = {
'title': title,
'plot': result.get("Overview"),
'mediatype': mediatype
}
tv_show_name = result.get("SeriesName")
if tv_show_name is not None:
details['tvshowtitle'] = tv_show_name
if item_type == "episode":
episode_number = result.get("IndexNumber", -1)
details["episode"] = str(episode_number)
season_number = result.get("ParentIndexNumber", -1)
details["season"] = str(season_number)
elif item_type == "season":
season_number = result.get("IndexNumber", -1)
details["season"] = str(season_number)
details["plotoutline"] = "jellyfin_id:%s" % (item_id,)
list_item.setInfo("Video", infoLabels=details)
return list_item
# For transcoding only
# Present the list of audio and subtitles to select from
# for external streamable subtitles add the URL to the Kodi item and let Kodi handle it
# else ask for the subtitles to be burnt in when transcoding
def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, subtitle_stream_index):
dialog = xbmcgui.Dialog()
audio_streams_list = {}
audio_streams = []
subtitle_streams_list = {}
subtitle_streams = ['No subtitles']
downloadable_streams = []
select_audio_index = audio_stream_index
select_subs_index = subtitle_stream_index
playurlprefs = ""
default_audio = media_source.get('DefaultAudioStreamIndex', 1)
default_sub = media_source.get('DefaultSubtitleStreamIndex', "")
source_id = media_source["Id"]
media_streams = media_source['MediaStreams']
for stream in media_streams:
# Since Jellyfin returns all possible tracks together, have to sort them.
index = stream['Index']
if 'Audio' in stream['Type']:
codec = stream['Codec']
channel_layout = stream.get('ChannelLayout', "")
try:
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channel_layout)
except:
track = "%s - %s %s" % (index, codec, channel_layout)
audio_streams_list[track] = index
audio_streams.append(track)
elif 'Subtitle' in stream['Type']:
try:
track = "%s - %s" % (index, stream['Language'])
except:
track = "%s - %s" % (index, stream['Codec'])
default = stream['IsDefault']
forced = stream['IsForced']
downloadable = stream['IsTextSubtitleStream'] and stream['IsExternal'] and stream['SupportsExternalStream']
if default:
track = "%s - Default" % track
if forced:
track = "%s - Forced" % track
if downloadable:
downloadable_streams.append(index)
subtitle_streams_list[track] = index
subtitle_streams.append(track)
# set audio index
if select_audio_index is not None:
playurlprefs += "&AudioStreamIndex=%s" % select_audio_index
elif len(audio_streams) > 1:
resp = dialog.select(string_load(30291), audio_streams)
if resp > -1:
# User selected audio
selected = audio_streams[resp]
select_audio_index = audio_streams_list[selected]
playurlprefs += "&AudioStreamIndex=%s" % select_audio_index
else: # User backed out of selection
playurlprefs += "&AudioStreamIndex=%s" % default_audio
# set subtitle index
if select_subs_index is not None:
# Load subtitles in the listitem if downloadable
if select_subs_index in downloadable_streams:
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
subtitle_url = subtitle_url % (download_utils.get_server(), item_id, source_id, select_subs_index)
log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url))
list_item.setSubtitles([subtitle_url])
else:
# Burn subtitles
playurlprefs += "&SubtitleStreamIndex=%s" % select_subs_index
elif len(subtitle_streams) > 1:
resp = dialog.select(string_load(30292), subtitle_streams)
if resp == 0:
# User selected no subtitles
pass
elif resp > -1:
# User selected subtitles
selected = subtitle_streams[resp]
select_subs_index = subtitle_streams_list[selected]
# Load subtitles in the listitem if downloadable
if select_subs_index in downloadable_streams:
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
subtitle_url = subtitle_url % (download_utils.get_server(), item_id, source_id, select_subs_index)
log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url))
list_item.setSubtitles([subtitle_url])
else:
# Burn subtitles
playurlprefs += "&SubtitleStreamIndex=%s" % select_subs_index
else: # User backed out of selection
playurlprefs += "&SubtitleStreamIndex=%s" % default_sub
if url.find("|verifypeer=false") != -1:
new_url = url.replace("|verifypeer=false", playurlprefs + "|verifypeer=false")
else:
new_url = url + playurlprefs
return new_url
# direct stream, set any available subtitle streams
def external_subs(media_source, list_item, item_id):
media_streams = media_source.get('MediaStreams')
if media_streams is None:
return
externalsubs = []
sub_names = []
for stream in media_streams:
if (stream['Type'] == "Subtitle"
and stream['IsExternal']
and stream['IsTextSubtitleStream']
and stream['SupportsExternalStream']):
index = stream['Index']
source_id = media_source['Id']
server = download_utils.get_server()
token = download_utils.authenticate()
language = stream.get('Language', '')
codec = stream.get('Codec', '')
url_root = '{}/Videos/{}/{}/Subtitles/{}'.format(server, item_id, source_id, index)
if language:
url = '{}/0/Stream.{}.{}?api_key={}'.format(
url_root, language, codec, token)
else:
url = '{}/0/Stream.{}?api_key={}'.format(url_root, codec, token)
default = ""
if stream['IsDefault']:
default = "default"
forced = ""
if stream['IsForced']:
forced = "forced"
sub_name = '{} ( {} ) {} {}'.format(language, codec, default, forced)
sub_names.append(sub_name)
externalsubs.append(url)
if len(externalsubs) == 0:
return
settings = xbmcaddon.Addon()
direct_stream_sub_select = settings.getSetting("direct_stream_sub_select")
if direct_stream_sub_select == "0" or (len(externalsubs) == 1 and not direct_stream_sub_select == "2"):
list_item.setSubtitles(externalsubs)
else:
resp = xbmcgui.Dialog().select(string_load(30292), sub_names)
if resp > -1:
selected_sub = externalsubs[resp]
log.debug("External Subtitle Selected: {0}".format(selected_sub))
list_item.setSubtitles([selected_sub])
def send_progress():
home_window = HomeWindow()
play_data = get_playing_data()
if play_data is None:
return
log.debug("Sending Progress Update")
player = xbmc.Player()
item_id = play_data.get("item_id")
if item_id is None:
return
play_time = player.getTime()
total_play_time = player.getTotalTime()
play_data["current_position"] = play_time
play_data["duration"] = total_play_time
play_data["currently_playing"] = True
home_window.set_property('now_playing', json.dumps(play_data))
source_id = play_data.get("source_id")
ticks = int(play_time * 10000000)
duration = int(total_play_time * 10000000)
paused = play_data.get("paused", False)
playback_type = play_data.get("playback_type")
play_session_id = play_data.get("play_session_id")
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist_position = playlist.getposition()
playlist_size = playlist.size()
volume, muted = get_volume()
postdata = {
'QueueableMediaTypes': "Video",
'CanSeek': True,
'ItemId': item_id,
'MediaSourceId': source_id,
'PositionTicks': ticks,
'RunTimeTicks': duration,
'IsPaused': paused,
'IsMuted': muted,
'PlayMethod': playback_type,
'PlaySessionId': play_session_id,
'PlaylistIndex': playlist_position,
'PlaylistLength': playlist_size,
'VolumeLevel': volume
}
log.debug("Sending POST progress started: {0}".format(postdata))
url = "{server}/Sessions/Playing/Progress"
download_utils.download_url(url, post_body=postdata, method="POST")
def get_volume():
json_data = xbmc.executeJSONRPC(
'{ "jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume", "muted"]}, "id": 1 }')
result = json.loads(json_data)
result = result.get('result', {})
volume = result.get('volume')
muted = result.get('muted')
return volume, muted
def prompt_for_stop_actions(item_id, data):
log.debug("prompt_for_stop_actions Called : {0}".format(data))
settings = xbmcaddon.Addon()
current_position = data.get("current_position", 0)
duration = data.get("duration", 0)
next_episode = data.get("next_episode")
item_type = data.get("item_type")
can_delete = data.get("can_delete", False)
prompt_next_percentage = int(settings.getSetting('promptPlayNextEpisodePercentage'))
play_prompt = settings.getSetting('promptPlayNextEpisodePercentage_prompt') == "true"
prompt_delete_episode_percentage = int(settings.getSetting('promptDeleteEpisodePercentage'))
prompt_delete_movie_percentage = int(settings.getSetting('promptDeleteMoviePercentage'))
# everything is off so return
if (prompt_next_percentage == 100 and
prompt_delete_episode_percentage == 100 and
prompt_delete_movie_percentage == 100):
return
prompt_to_delete = False
# if no runtime we cant calculate perceantge so just return
if duration == 0:
log.debug("No duration so returing")
return
# item percentage complete
percenatge_complete = int((current_position / duration) * 100)
log.debug("Episode Percentage Complete: {0}".format(percenatge_complete))
if (can_delete and
prompt_delete_episode_percentage < 100 and
item_type == "Episode" and
percenatge_complete > prompt_delete_episode_percentage):
prompt_to_delete = True
if (can_delete and
prompt_delete_movie_percentage < 100 and
item_type == "Movie" and
percenatge_complete > prompt_delete_movie_percentage):
prompt_to_delete = True
# prompt for next episode
if (next_episode is not None and
prompt_next_percentage < 100 and
item_type == "Episode" and
percenatge_complete > prompt_next_percentage):
index = next_episode.get("IndexNumber", -1)
if play_prompt:
plugin_path = settings.getAddonInfo('path')
plugin_path_real = xbmc.translatePath(os.path.join(plugin_path))
play_next_dialog = PlayNextDialog("PlayNextDialog.xml", plugin_path_real, "default", "720p")
play_next_dialog.set_episode_info(next_episode)
play_next_dialog.doModal()
if not play_next_dialog.get_play_called():
xbmc.executebuiltin("Container.Refresh")
def stop_all_playback():
home_window = HomeWindow()
played_information_string = home_window.get_property('played_information')
if played_information_string:
played_information = json.loads(played_information_string)
else:
played_information = {}
log.debug("stop_all_playback : {0}".format(played_information))
if len(played_information) == 0:
return
log.debug("played_information: {0}".format(played_information))
clear_entries = []
home_window.clear_property("currently_playing_id")
for item in played_information:
data = played_information.get(item)
if data.get("currently_playing", False) is True:
log.debug("item_data: {0}".format(data))
current_position = data.get("current_position", 0)
duration = data.get("duration", 0)
jellyfin_item_id = data.get("item_id")
jellyfin_source_id = data.get("source_id")
play_session_id = data.get("play_session_id")
if jellyfin_item_id is not None and current_position >= 0:
log.debug("Playback Stopped at: {0}".format(current_position))
url = "{server}/Sessions/Playing/Stopped"
postdata = {
'ItemId': jellyfin_item_id,
'MediaSourceId': jellyfin_source_id,
'PositionTicks': int(current_position * 10000000),
'RunTimeTicks': int(duration * 10000000),
'PlaySessionId': play_session_id
}
download_utils.download_url(url, post_body=postdata, method="POST")
data["currently_playing"] = False
if data.get("play_action_type", "") == "play":
prompt_for_stop_actions(jellyfin_item_id, data)
clear_entries.append(item)
if data.get('playback_type') == 'Transcode':
device_id = ClientInformation().get_device_id()
url = "{server}/Videos/ActiveEncodings?DeviceId=%s" % device_id
download_utils.download_url(url, method="DELETE")
for entry in clear_entries:
del played_information[entry]
home_window.set_property('played_information', json.dumps(played_information))
def get_playing_data():
player = xbmc.Player()
home_window = HomeWindow()
play_data_string = home_window.get_property('now_playing')
play_data = json.loads(play_data_string)
played_information_string = home_window.get_property('played_information')
if played_information_string:
played_information = json.loads(played_information_string)
else:
played_information = {}
playlist_data_string = home_window.get_property('playlist')
if playlist_data_string:
playlist_data = json.loads(playlist_data_string)
else:
playlist_data = {}
item_id = play_data.get("item_id")
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
try:
playing_file = player.getPlayingFile()
except Exception as e:
log.error("get_playing_data : getPlayingFile() : {0}".format(e))
return None
log.debug("get_playing_data : getPlayingFile() : {0}".format(playing_file))
if server in playing_file and item_id is not None:
play_time = player.getTime()
total_play_time = player.getTotalTime()
if item_id is not None and item_id not in playing_file and playing_file in playlist_data:
# if the current now_playing data isn't correct, pull it from the playlist_data
play_data = playlist_data.pop(playing_file)
# Update now_playing data
home_window.set_property('playlist', json.dumps(playlist_data))
play_data["current_position"] = play_time
play_data["duration"] = total_play_time
played_information[item_id] = play_data
home_window.set_property('now_playing', json.dumps(play_data))
home_window.set_property('played_information', json.dumps(played_information))
return play_data
return {}
class Service(xbmc.Player):
def __init__(self, *args):
log.debug("Starting monitor service: {0}".format(args))
def onPlayBackStarted(self):
# Will be called when xbmc starts playing a file
stop_all_playback()
if not xbmc.Player().isPlaying():
log.debug("onPlayBackStarted: not playing file!")
return
play_data = get_playing_data()
if play_data is None:
return
play_data["paused"] = False
play_data["currently_playing"] = True
jellyfin_item_id = play_data["item_id"]
jellyfin_source_id = play_data["source_id"]
playback_type = play_data["playback_type"]
play_session_id = play_data["play_session_id"]
# if we could not find the ID of the current item then return
if jellyfin_item_id is None:
return
home_window = HomeWindow()
played_information_string = home_window.get_property('played_information')
played_information = json.loads(played_information_string)
played_information[jellyfin_item_id] = play_data
home_window.set_property('played_information', json.dumps(played_information))
log.debug("Sending Playback Started")
postdata = {
'QueueableMediaTypes': "Video",
'CanSeek': True,
'ItemId': jellyfin_item_id,
'MediaSourceId': jellyfin_source_id,
'PlayMethod': playback_type,
'PlaySessionId': play_session_id
}
log.debug("Sending POST play started: {0}".format(postdata))
url = "{server}/Sessions/Playing"
download_utils.download_url(url, post_body=postdata, method="POST")
home_screen = HomeWindow()
home_screen.set_property("currently_playing_id", str(jellyfin_item_id))
def onPlayBackEnded(self):
# Will be called when kodi stops playing a file
log.debug("onPlayBackEnded")
stop_all_playback()
def onPlayBackStopped(self):
# Will be called when user stops kodi playing a file
log.debug("onPlayBackStopped")
stop_all_playback()
def onPlayBackPaused(self):
# Will be called when kodi pauses the video
log.debug("onPlayBackPaused")
play_data = get_playing_data()
if play_data is not None:
play_data['paused'] = True
send_progress()
def onPlayBackResumed(self):
# Will be called when kodi resumes the video
log.debug("onPlayBackResumed")
play_data = get_playing_data()
if play_data is not None:
play_data['paused'] = False
send_progress()
def onPlayBackSeek(self, time, seek_offset):
# Will be called when kodi seeks in video
log.debug("onPlayBackSeek")
send_progress()
class PlaybackService(xbmc.Monitor):
background_image_cache_thread = None
def __init__(self, monitor):
self.monitor = monitor
def onNotification(self, sender, method, data):
if method == 'GUI.OnScreensaverActivated':
self.screensaver_activated()
return
elif method == 'GUI.OnScreensaverDeactivated':
self.screensaver_deactivated()
return
elif method == 'System.OnQuit':
home_window = HomeWindow()
home_window.set_property('exit', 'True')
return
if sender != 'plugin.video.jellycon':
return
signal = method.split('.', 1)[-1]
if signal not in ("jellycon_play_action", "jellycon_play_youtube_trailer_action", "set_view"):
return
data_json = json.loads(data)
play_info = data_json[0]
log.debug("PlaybackService:onNotification:{0}".format(play_info))
if signal == "jellycon_play_action":
play_file(play_info)
elif signal == "jellycon_play_youtube_trailer_action":
trailer_link = play_info["url"]
xbmc.executebuiltin(trailer_link)
elif signal == "set_view":
view_id = play_info["view_id"]
log.debug("Setting view id: {0}".format(view_id))
xbmc.executebuiltin("Container.SetViewMode(%s)" % int(view_id))
def screensaver_activated(self):
log.debug("Screen Saver Activated")
home_screen = HomeWindow()
home_screen.clear_property("skip_select_user")
settings = xbmcaddon.Addon()
stop_playback = settings.getSetting("stopPlaybackOnScreensaver") == 'true'
if stop_playback:
player = xbmc.Player()
if player.isPlayingVideo():
log.debug("Screen Saver Activated : isPlayingVideo() = true")
play_data = get_playing_data()
if play_data:
log.debug("Screen Saver Activated : this is an JellyCon item so stop it")
player.stop()
clear_old_cache_data()
cache_images = settings.getSetting('cacheImagesOnScreenSaver') == 'true'
if cache_images:
self.background_image_cache_thread = CacheArtwork()
self.background_image_cache_thread.start()
def screensaver_deactivated(self):
log.debug("Screen Saver Deactivated")
if self.background_image_cache_thread:
self.background_image_cache_thread.stop_activity()
self.background_image_cache_thread = None
settings = xbmcaddon.Addon()
show_change_user = settings.getSetting('changeUserOnScreenSaver') == 'true'
if show_change_user:
home_screen = HomeWindow()
skip_select_user = home_screen.get_property("skip_select_user")
if skip_select_user is not None and skip_select_user == "true":
return
xbmc.executebuiltin("RunScript(plugin.video.jellycon,0,?mode=CHANGE_USER)")