Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Show "SDH" (Subtitles for Deaf and Hard of Hearing) label in subtitle selection dialog when track has IsHearingImpaired flag set.
1947 lines
71 KiB
Python
1947 lines
71 KiB
Python
from __future__ import (
|
|
division, absolute_import, print_function, unicode_literals
|
|
)
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import binascii
|
|
import datetime
|
|
|
|
import xbmc
|
|
import xbmcgui
|
|
import xbmcaddon
|
|
import xbmcvfs
|
|
import xbmcplugin
|
|
from six.moves.urllib.parse import urlencode
|
|
|
|
from .jellyfin import api
|
|
from .lazylogger import LazyLogger
|
|
from .dialogs import ResumeDialog
|
|
from .utils import send_event_notification, convert_size, get_device_id, translate_string, load_user_details, translate_path, get_jellyfin_url, download_external_sub, get_bitrate
|
|
from .kodi_utils import HomeWindow
|
|
from .datamanager import clear_old_cache_data
|
|
from .item_functions import extract_item_info, add_gui_item, get_art
|
|
from .cache_images import CacheArtwork
|
|
from .picture_viewer import PictureViewer
|
|
from .tracking import timer
|
|
from .playnext import PlayNextDialog
|
|
|
|
log = LazyLogger(__name__)
|
|
settings = xbmcaddon.Addon()
|
|
|
|
|
|
def play_all_files(items, play_items=True):
|
|
home_window = HomeWindow()
|
|
log.debug("playAllFiles called with items: {0}", items)
|
|
server = settings.getSetting('server_address')
|
|
|
|
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
|
playlist.clear()
|
|
|
|
playlist_data = {}
|
|
|
|
for item in items:
|
|
|
|
item_id = item.get("Id")
|
|
|
|
# get playback info
|
|
playback_info = 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(translate_string(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 = 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", translate_string(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 and playlist.size() == 1:
|
|
# Play the first item immediately before processing the rest
|
|
xbmc.Player().play(playlist)
|
|
|
|
if play_items:
|
|
# Should already be playing, don't need to return anything
|
|
return None
|
|
else:
|
|
return playlist
|
|
|
|
|
|
def play_list_of_items(id_list):
|
|
log.debug("Loading all items in the list")
|
|
items = []
|
|
|
|
for item_id in id_list:
|
|
url = "/Users/{}/Items/{}?format=json".format(api.user_id, item_id)
|
|
result = api.get(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 = settings.getSetting('server_address')
|
|
|
|
item_id = play_info.get("item_id")
|
|
|
|
url = "/Users/{}/Items/{}?format=json".format(api.user_id, item_id)
|
|
item = api.get(url)
|
|
if item is None:
|
|
log.debug("Playfile item was None, so can not play!")
|
|
return
|
|
|
|
# get playback info
|
|
playback_info = 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(translate_string(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 = 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", translate_string(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")
|
|
url = "/Users/{}/Items/{}/Intros".format(api.user_id, item_id)
|
|
intro_items = api.get(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")
|
|
|
|
channel_id = None
|
|
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)
|
|
force_track_selection = play_info.get("force_track_selection", False)
|
|
|
|
log.debug("playFile id({0}) resume({1}) force_transcode({2}) force_track_selection({3})".format(
|
|
item_id, auto_resume, force_transcode, force_track_selection))
|
|
|
|
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 = settings.getSetting('server_address')
|
|
|
|
url = "/Users/{}/Items/{}?format=json".format(api.user_id, item_id)
|
|
result = api.get(url)
|
|
log.debug("Playfile item: {0}".format(result))
|
|
|
|
if result is None:
|
|
log.debug("Playfile item was None, so can not play!")
|
|
return
|
|
|
|
# Generate an instant mix based on the item
|
|
if action == 'instant_mix':
|
|
max_queue = int(settings.getSetting('max_play_queue'))
|
|
url_root = '/Items/{}/InstantMix'.format(item_id)
|
|
url_params = {
|
|
'UserId': api.user_id,
|
|
'Fields': 'MediaSources',
|
|
'IncludeItemTypes': 'Audio',
|
|
'SortBy': 'SortName',
|
|
'limit': max_queue
|
|
}
|
|
url = get_jellyfin_url(url_root, url_params)
|
|
result = api.get(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 season, playlist, artist, album, or a full library then play
|
|
*all* items in that parent.
|
|
* Taking the max queue size setting into account
|
|
'''
|
|
if result.get("Type") in ["Season", "Series", "MusicArtist", "MusicAlbum",
|
|
"Playlist", "CollectionFolder", "MusicGenre"]:
|
|
max_queue = int(settings.getSetting('max_play_queue'))
|
|
log.debug("PlayAllFiles for parent item id: {0}".format(item_id))
|
|
url_root = '/Users/{}/Items'.format(api.user_id)
|
|
# Look specifically for episodes or audio files
|
|
url_params = {
|
|
'Fields': 'MediaSources',
|
|
'IncludeItemTypes': 'Episode,Audio',
|
|
'Recursive': True,
|
|
'SortBy': 'SortName',
|
|
'limit': max_queue
|
|
}
|
|
if result.get("Type") == "MusicGenre":
|
|
url_params['genreIds'] = item_id
|
|
else:
|
|
url_params['ParentId'] = item_id
|
|
|
|
if action == 'shuffle':
|
|
url_params['SortBy'] = 'Random'
|
|
|
|
url = get_jellyfin_url(url_root, url_params)
|
|
result = api.get(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 = "/Users/{}/Items/{}?format=json".format(api.user_id, channel_id)
|
|
result = api.get(url)
|
|
item_id = result["Id"]
|
|
elif result.get('Type') == "TvChannel":
|
|
channel_id = result.get("Id")
|
|
url = "/Users/{}/Items/{}?format=json".format(api.user_id, channel_id)
|
|
result = api.get(url)
|
|
item_id = result["Id"]
|
|
elif result.get("Type") == "Photo":
|
|
play_url = "%s/Items/%s/Images/Primary"
|
|
play_url = play_url % (server, item_id)
|
|
|
|
plugin_path = translate_path(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 = 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(translate_string(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(translate_string(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 = round(reasonable_ticks / 10000,0)
|
|
display_time = (datetime.datetime(1,1,1) + datetime.timedelta(seconds=seek_time)).strftime('%H:%M:%S')
|
|
|
|
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 = get_play_url(selected_media_source, play_session_id, channel_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", translate_string(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_id, 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, force_track_selection)
|
|
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
|
|
send_next_episode_details(result, next_episode)
|
|
|
|
# We need the livestream id to properly delete encodings
|
|
if result.get("Type", "") in ["Program", "TvChannel"]:
|
|
for media_source in media_sources:
|
|
livestream_id = media_source.get("LiveStreamId")
|
|
data["livestream_id"] = livestream_id
|
|
if livestream_id:
|
|
break
|
|
|
|
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)
|
|
player.play(playlist)
|
|
else:
|
|
if len(sys.argv) > 1 and int(sys.argv[1]) > 0:
|
|
# Play from info menu
|
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, list_item)
|
|
else:
|
|
'''
|
|
Play from remote control or addon menus. Doesn't have a handle,
|
|
so need to call player directly
|
|
'''
|
|
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 = ('/Users/{}/Items?'.format(api.user_id) +
|
|
'?Recursive=true' +
|
|
'&ParentId=' + parent_id +
|
|
'&IsVirtualUnaired=false' +
|
|
'&IsMissing=False' +
|
|
'&IncludeItemTypes=Episode' +
|
|
'&ImageTypeLimit=1' +
|
|
'&format=json')
|
|
|
|
items_result = api.get(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"] = settings.getSetting('server_address')
|
|
|
|
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,
|
|
'artist': "Unknown Artist",
|
|
'album': "Unknown Album"
|
|
}
|
|
artist = result.get("Artists", [])
|
|
if artist:
|
|
details['artist'] = artist[0]
|
|
track = result.get("IndexNumber")
|
|
if track:
|
|
details['tracknumber'] = track
|
|
album = result.get("Album")
|
|
if album:
|
|
details['album'] = album
|
|
|
|
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, force_track_selection=False):
|
|
dialog = xbmcgui.Dialog()
|
|
audio_streams_list = {}
|
|
audio_streams = []
|
|
audio_streams_data = [] # Store full stream data for preference matching
|
|
subtitle_streams_list = {}
|
|
subtitle_streams = ['No subtitles']
|
|
subtitle_streams_data = [] # Store full stream data for preference matching
|
|
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"]
|
|
|
|
# Read user preferences
|
|
# Map select index to language code
|
|
lang_index_map = ['ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # '' = other/custom
|
|
sub_lang_index_map = ['', 'ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # First is 'none'
|
|
|
|
audio_lang_index = int(settings.getSetting("preferred_audio_language") or "0")
|
|
preferred_audio_lang = lang_index_map[audio_lang_index] if audio_lang_index < len(lang_index_map) else ''
|
|
|
|
auto_select_default_audio = settings.getSetting("auto_select_default_audio") == "true"
|
|
|
|
sub_lang_index = int(settings.getSetting("preferred_subtitle_language") or "1")
|
|
preferred_sub_lang = sub_lang_index_map[sub_lang_index] if sub_lang_index < len(sub_lang_index_map) else ''
|
|
|
|
prefer_forced = settings.getSetting("prefer_forced_subtitles") == "true"
|
|
only_forced = settings.getSetting("only_forced_subtitles") == "true"
|
|
|
|
# Subtitle codec preference: 0=PGS, 1=SRT, 2=No preference
|
|
codec_pref_index = int(settings.getSetting("subtitle_codec_preference") or "0")
|
|
prefer_pgs = codec_pref_index == 0
|
|
prefer_srt = codec_pref_index == 1
|
|
# codec_pref_index == 2 means no preference (both prefer_pgs and prefer_srt are False)
|
|
|
|
auto_no_subs = settings.getSetting("auto_no_subtitles_if_no_match") == "true"
|
|
|
|
# Language code mapping for better matching
|
|
language_map = {
|
|
'ger': ['ger', 'deu', 'de', 'german', 'deutsch'],
|
|
'eng': ['eng', 'en', 'english'],
|
|
'fra': ['fra', 'fr', 'fre', 'french', 'français'],
|
|
'spa': ['spa', 'es', 'spanish', 'español'],
|
|
'ita': ['ita', 'it', 'italian', 'italiano'],
|
|
'jpn': ['jpn', 'ja', 'japanese', '日本語'],
|
|
'rus': ['rus', 'ru', 'russian'],
|
|
}
|
|
|
|
# Helper function to check if language matches
|
|
def language_matches(stream_lang, preferred_lang):
|
|
stream_lang = stream_lang.lower()
|
|
preferred_lang = preferred_lang.lower()
|
|
|
|
# Direct match
|
|
if preferred_lang in stream_lang or stream_lang in preferred_lang:
|
|
return True
|
|
|
|
# Check against language map
|
|
for key, variations in language_map.items():
|
|
if preferred_lang in variations:
|
|
# Check if stream language matches any variation
|
|
for variant in variations:
|
|
if variant in stream_lang or stream_lang.startswith(variant[:2]):
|
|
return True
|
|
|
|
return False
|
|
|
|
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.get('Codec', None)
|
|
channel_layout = stream.get('ChannelLayout', "")
|
|
title = stream.get('Title', '')
|
|
|
|
if not codec:
|
|
# Probably tvheadend and has no other info
|
|
track = "%s - default" % (index)
|
|
else:
|
|
try:
|
|
# Track includes language
|
|
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channel_layout)
|
|
except KeyError:
|
|
# Track doesn't include language
|
|
track = "%s - %s %s" % (index, codec, channel_layout)
|
|
|
|
# Add title if available
|
|
if title:
|
|
track = "%s - %s" % (track, title)
|
|
|
|
audio_streams_list[track] = index
|
|
audio_streams.append(track)
|
|
audio_streams_data.append(stream)
|
|
|
|
elif 'Subtitle' in stream['Type']:
|
|
language = stream.get('Language', 'Unknown')
|
|
codec = stream.get('Codec', 'Unknown')
|
|
|
|
codec_names = {
|
|
'subrip': 'SRT',
|
|
'hdmv_pgs_subtitle': 'PGS',
|
|
'dvd_subtitle': 'VobSub',
|
|
'ass': 'ASS'
|
|
}
|
|
codec_display = codec_names.get(codec.lower(), codec.upper())
|
|
|
|
track = "%s - %s (%s)" % (index, language, codec_display)
|
|
|
|
default = stream['IsDefault']
|
|
forced = stream['IsForced']
|
|
hearing_impaired = stream.get('IsHearingImpaired', False)
|
|
downloadable = stream['IsTextSubtitleStream'] and stream['IsExternal'] and stream['SupportsExternalStream']
|
|
|
|
if default:
|
|
track = "%s - Default" % track
|
|
if forced:
|
|
track = "%s - Forced" % track
|
|
if hearing_impaired:
|
|
track = "%s - SDH" % track
|
|
if downloadable:
|
|
downloadable_streams.append(index)
|
|
|
|
subtitle_streams_list[track] = index
|
|
subtitle_streams.append(track)
|
|
subtitle_streams_data.append(stream)
|
|
|
|
# Auto-select audio track based on preferences
|
|
# Only auto-select if not already set by server/remote control AND not forcing manual selection
|
|
if not force_track_selection and select_audio_index is None and len(audio_streams_data) > 0 and (preferred_audio_lang or auto_select_default_audio):
|
|
auto_selected = None
|
|
best_score = -1
|
|
|
|
# Try to match preferred language with scoring
|
|
if preferred_audio_lang:
|
|
for stream in audio_streams_data:
|
|
score = 0
|
|
stream_lang = stream.get('Language', '')
|
|
|
|
# Match against common variations using language map
|
|
if language_matches(stream_lang, preferred_audio_lang):
|
|
score += 100 # Language match
|
|
|
|
# Bonus for default track
|
|
if stream.get('IsDefault', False):
|
|
score += 50
|
|
|
|
# Bonus based on channel count (more channels = better quality)
|
|
channels = stream.get('Channels', 2)
|
|
score += min(channels * 5, 40) # Max +40 for 8 channels
|
|
|
|
# Bonus for high-quality codecs
|
|
codec = stream.get('Codec', '').lower()
|
|
if 'truehd' in codec or 'dts-hd' in codec or 'dts-ma' in codec:
|
|
score += 30
|
|
elif 'dts' in codec:
|
|
score += 20
|
|
elif 'ac3' in codec or 'eac3' in codec:
|
|
score += 10
|
|
|
|
# Penalty for commentary tracks
|
|
title = stream.get('Title', '').lower()
|
|
display_title = stream.get('DisplayTitle', '').lower()
|
|
if 'commentary' in title or 'kommentar' in title or 'commentary' in display_title:
|
|
score -= 100 # Effectively exclude commentary
|
|
|
|
log.debug("Audio score for {0} ({1}): {2} (channels={3}, codec={4}, default={5})".format(
|
|
stream_lang, stream['Index'], score, channels, codec, stream.get('IsDefault', False)))
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
auto_selected = stream['Index']
|
|
|
|
# Fall back to default audio if enabled and no language match
|
|
if auto_selected is None and auto_select_default_audio:
|
|
auto_selected = default_audio
|
|
log.debug("Auto-selected default audio (index {0})".format(auto_selected))
|
|
|
|
if auto_selected is not None:
|
|
select_audio_index = auto_selected
|
|
if best_score > 0:
|
|
log.debug("Auto-selected audio (index {0}, score {1})".format(auto_selected, best_score))
|
|
|
|
# Auto-select subtitle track based on preferences
|
|
# Only auto-select if not already set by server/remote control AND not forcing manual selection
|
|
if force_track_selection:
|
|
log.debug("Forcing manual track selection (from context menu)")
|
|
elif select_subs_index is not None:
|
|
log.debug("Using subtitle index from server/remote: {0}".format(select_subs_index))
|
|
elif len(subtitle_streams_data) == 0:
|
|
log.debug("No subtitle streams available")
|
|
elif len(subtitle_streams_data) > 0 and preferred_sub_lang: # Only if user configured a language preference
|
|
auto_selected = None
|
|
best_score = -1
|
|
|
|
log.debug("Auto-selecting subtitle: preferred_lang={0}, prefer_forced={1}, only_forced={2}, prefer_pgs={3}, prefer_srt={4}".format(
|
|
preferred_sub_lang, prefer_forced, only_forced, prefer_pgs, prefer_srt))
|
|
|
|
for stream in subtitle_streams_data:
|
|
score = 0
|
|
stream_lang = stream.get('Language', '')
|
|
codec = stream.get('Codec', '').lower()
|
|
is_forced = stream.get('IsForced', False)
|
|
is_default = stream.get('IsDefault', False)
|
|
|
|
# Skip non-forced subtitles if only_forced is enabled
|
|
if only_forced and not is_forced:
|
|
log.debug("Skipping non-forced subtitle {0} ({1}) - only_forced is enabled".format(
|
|
stream_lang, stream['Index']))
|
|
continue
|
|
|
|
# Score based on language match
|
|
if preferred_sub_lang and language_matches(stream_lang, preferred_sub_lang):
|
|
score += 100
|
|
|
|
# Bonus for forced if preferred
|
|
if prefer_forced and is_forced:
|
|
score += 50
|
|
|
|
# Bonus for codec preference
|
|
if prefer_pgs and codec in ['hdmv_pgs_subtitle', 'pgs']:
|
|
score += 30
|
|
elif prefer_srt and codec in ['subrip', 'srt']:
|
|
score += 30
|
|
|
|
# Small bonus for default
|
|
if is_default:
|
|
score += 10
|
|
|
|
log.debug("Subtitle score for {0} ({1}): {2} (forced={3}, codec={4})".format(
|
|
stream_lang, stream['Index'], score, is_forced, codec))
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
auto_selected = stream['Index']
|
|
|
|
if auto_selected is not None and best_score >= 100: # Only auto-select if language matched
|
|
select_subs_index = auto_selected
|
|
log.debug("Auto-selected subtitle (index {0}, score {1})".format(auto_selected, best_score))
|
|
elif auto_no_subs and preferred_sub_lang: # No match found but user wants auto "no subs"
|
|
select_subs_index = -1 # Special value to indicate "no subtitles"
|
|
log.debug("No matching subtitle found - auto-selected 'No subtitles'")
|
|
|
|
# set audio index
|
|
if select_audio_index is not None:
|
|
playurlprefs += "&AudioStreamIndex=%s" % select_audio_index
|
|
|
|
elif len(audio_streams) > 1:
|
|
resp = dialog.select(translate_string(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:
|
|
# Handle special "no subtitles" value
|
|
if select_subs_index == -1:
|
|
# User wants no subtitles - do nothing
|
|
pass
|
|
# Load subtitles in the listitem if downloadable
|
|
elif select_subs_index in downloadable_streams:
|
|
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
|
|
subtitle_url = subtitle_url % (settings.getSetting('server_address'), 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&SubtitleMethod=Encode" % select_subs_index
|
|
|
|
elif len(subtitle_streams) > 1:
|
|
resp = dialog.select(translate_string(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 % (settings.getSetting('server_address'), 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&SubtitleMethod=Encode" % select_subs_index
|
|
|
|
else: # User backed out of selection
|
|
playurlprefs += "&SubtitleStreamIndex=%s&SubtitleMethod=Encode" % default_sub
|
|
|
|
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 = []
|
|
|
|
server = settings.getSetting('server_address')
|
|
for idx, stream in enumerate(media_streams):
|
|
if (stream['Type'] == "Subtitle"
|
|
and stream['IsExternal']
|
|
and stream['IsTextSubtitleStream']
|
|
and stream['SupportsExternalStream']):
|
|
|
|
language = stream.get('Language', '')
|
|
if language and stream['IsDefault']:
|
|
language = '{}.default'.format(language)
|
|
if language and stream['IsForced']:
|
|
language = '{}.forced'.format(language)
|
|
if language and stream['IsHearingImpaired']:
|
|
language = '{}.SDH'.format(language)
|
|
codec = stream.get('Codec', '')
|
|
|
|
url = '{}{}'.format(server, stream.get('DeliveryUrl'))
|
|
if language:
|
|
title = str(idx)
|
|
if stream.get('Title'):
|
|
title = stream['Title']
|
|
'''
|
|
Starting in 10.8, the server no longer provides language
|
|
specific download points. We have to download the file
|
|
and name it with the language code ourselves so Kodi
|
|
will parse it correctly
|
|
'''
|
|
subtitle_file = download_external_sub(language, codec, url, title)
|
|
else:
|
|
# If there is no language defined, we can go directly to the server
|
|
subtitle_file = url
|
|
|
|
sub_name = '{} ( {} )'.format(language, codec)
|
|
|
|
sub_names.append(sub_name)
|
|
externalsubs.append(subtitle_file)
|
|
|
|
if len(externalsubs) == 0:
|
|
return
|
|
|
|
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(translate_string(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 = "/Sessions/Playing/Progress"
|
|
api.post(url, postdata)
|
|
|
|
|
|
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))
|
|
|
|
current_position = data.get("current_position", 0)
|
|
duration = data.get("duration", 0)
|
|
next_episode = data.get("next_episode")
|
|
item_type = data.get("item_type")
|
|
|
|
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
|
|
|
|
# if no runtime we can't calculate percentage so just return
|
|
if duration == 0:
|
|
log.debug("No duration so returning")
|
|
return
|
|
|
|
# item percentage complete
|
|
percentage_complete = int((current_position / duration) * 100)
|
|
log.debug("Episode Percentage Complete: {0}".format(percentage_complete))
|
|
|
|
# prompt for next episode
|
|
if (next_episode is not None and
|
|
prompt_next_percentage < 100 and
|
|
item_type == "Episode" and
|
|
percentage_complete >= prompt_next_percentage):
|
|
|
|
if play_prompt:
|
|
plugin_path = settings.getAddonInfo('path')
|
|
plugin_path_real = translate_path(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")
|
|
|
|
else:
|
|
play_info = {
|
|
"item_id": next_episode.get("Id"),
|
|
"auto_resume": "-1",
|
|
"force_transcode": False
|
|
}
|
|
send_event_notification("jellycon_play_action", play_info)
|
|
|
|
|
|
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")
|
|
livestream_id = data.get('livestream_id')
|
|
|
|
if jellyfin_item_id is not None and current_position >= 0:
|
|
log.debug("Playback Stopped at: {0}".format(current_position))
|
|
|
|
url = "/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
|
|
}
|
|
|
|
# If this is a livestream, include the id in the stopped call
|
|
if livestream_id:
|
|
postdata['LiveStreamId'] = livestream_id
|
|
|
|
api.post(url, postdata)
|
|
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 = get_device_id()
|
|
url = "/Videos/ActiveEncodings?DeviceId=%s&playSessionId=%s" % (device_id, play_session_id)
|
|
api.delete(url)
|
|
|
|
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')
|
|
try:
|
|
play_data = json.loads(play_data_string)
|
|
except ValueError:
|
|
# This isn't a JellyCon item
|
|
return None
|
|
|
|
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")
|
|
|
|
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 {}
|
|
|
|
def get_jellyfin_playing_item():
|
|
home_window = HomeWindow()
|
|
play_data_string = home_window.get_property('now_playing')
|
|
try:
|
|
play_data = json.loads(play_data_string)
|
|
except ValueError:
|
|
# This isn't a JellyCon item
|
|
return None
|
|
|
|
return play_data.get("item_id")
|
|
|
|
def get_play_url(media_source, play_session_id, channel_id=None):
|
|
log.debug("get_play_url - media_source: {0}", media_source)
|
|
|
|
# check if strm file Container
|
|
if media_source.get('Container') == 'strm':
|
|
log.debug("Detected STRM Container")
|
|
playurl, listitem_props = get_strm_details(media_source)
|
|
if playurl is None:
|
|
log.debug("Error, no strm content")
|
|
return None, None, None
|
|
else:
|
|
return playurl, "0", listitem_props
|
|
|
|
# get all the options
|
|
server = settings.getSetting('server_address')
|
|
allow_direct_file_play = settings.getSetting('allow_direct_file_play') == 'true'
|
|
|
|
can_direct_play = media_source["SupportsDirectPlay"]
|
|
can_direct_stream = media_source["SupportsDirectStream"]
|
|
can_transcode = media_source["SupportsTranscoding"]
|
|
|
|
playurl = None
|
|
playback_type = None
|
|
|
|
# check if file can be directly played
|
|
if allow_direct_file_play and can_direct_play:
|
|
direct_path = media_source["Path"]
|
|
direct_path = direct_path.replace("\\", "/")
|
|
direct_path = direct_path.strip()
|
|
|
|
# handle DVD structure
|
|
container = media_source["Container"]
|
|
if container == "dvd":
|
|
direct_path = direct_path + "/VIDEO_TS/VIDEO_TS.IFO"
|
|
elif container == "bluray":
|
|
direct_path = direct_path + "/BDMV/index.bdmv"
|
|
|
|
if direct_path.startswith("//"):
|
|
direct_path = "smb://" + direct_path[2:]
|
|
|
|
log.debug("playback_direct_path: {0}".format(direct_path))
|
|
|
|
if xbmcvfs.exists(direct_path):
|
|
playurl = direct_path
|
|
playback_type = "0"
|
|
|
|
# check if file can be direct streamed/played
|
|
if (can_direct_stream or can_direct_play) and playurl is None:
|
|
item_id = media_source.get('Id')
|
|
if channel_id:
|
|
# live tv has to be transcoded by the server
|
|
playurl = None
|
|
else:
|
|
url_root = '{}/Videos/{}/stream'.format(server, item_id)
|
|
play_params = {
|
|
'static': True,
|
|
'PlaySessionId': play_session_id,
|
|
'MediaSourceId': item_id,
|
|
}
|
|
play_param_string = urlencode(play_params)
|
|
playurl = '{}?{}'.format(url_root, play_param_string)
|
|
playback_type = "1"
|
|
|
|
# check is file can be transcoded
|
|
if can_transcode and playurl is None:
|
|
item_id = media_source.get('Id')
|
|
device_id = get_device_id()
|
|
|
|
user_details = load_user_details()
|
|
user_token = user_details.get('token')
|
|
bitrate = get_bitrate(settings.getSetting("force_max_stream_bitrate"))
|
|
playback_max_width = settings.getSetting("playback_max_width")
|
|
audio_codec = settings.getSetting("audio_codec")
|
|
audio_playback_bitrate = settings.getSetting("audio_playback_bitrate")
|
|
audio_bitrate = int(audio_playback_bitrate) * 1000
|
|
audio_max_channels = settings.getSetting("audio_max_channels")
|
|
playback_video_force_8 = settings.getSetting("playback_video_force_8") == "true"
|
|
|
|
# Determine target video codec for transcoding
|
|
transcode_target_codec_setting = settings.getSetting("transcode_target_video_codec")
|
|
if transcode_target_codec_setting == "1":
|
|
transcode_video_codec = "hevc"
|
|
elif transcode_target_codec_setting == "2":
|
|
transcode_video_codec = "av1"
|
|
else:
|
|
transcode_video_codec = "h264"
|
|
|
|
transcode_params = {
|
|
"MediaSourceId": item_id,
|
|
"DeviceId": device_id,
|
|
"PlaySessionId": play_session_id,
|
|
"api_key": user_token,
|
|
"SegmentContainer": "ts",
|
|
"VideoCodec": transcode_video_codec,
|
|
"VideoBitrate": bitrate,
|
|
"MaxWidth": playback_max_width,
|
|
"AudioCodec": audio_codec,
|
|
"TranscodingMaxAudioChannels": audio_max_channels,
|
|
"AudioBitrate": audio_bitrate
|
|
}
|
|
if playback_video_force_8:
|
|
transcode_params.update({"MaxVideoBitDepth": "8"})
|
|
|
|
# We need to include the channel ID if this is a live stream
|
|
if channel_id:
|
|
if media_source.get('LiveStreamId'):
|
|
transcode_params['LiveStreamId'] = media_source.get('LiveStreamId')
|
|
transcode_path = urlencode(transcode_params)
|
|
playurl = '{}/Videos/{}/master.m3u8?{}'.format(
|
|
server, channel_id, transcode_path)
|
|
else:
|
|
transcode_path = urlencode(transcode_params)
|
|
playurl = '{}/Videos/{}/master.m3u8?{}'.format(
|
|
server, item_id, transcode_path)
|
|
|
|
playback_type = "2"
|
|
|
|
return playurl, playback_type, []
|
|
|
|
|
|
def get_strm_details(media_source):
|
|
playurl = None
|
|
listitem_props = []
|
|
|
|
contents = media_source.get('Path').encode('utf-8') # contains contents of strm file with linebreaks
|
|
|
|
line_break = '\r'
|
|
if '\r\n' in contents:
|
|
line_break = '\r\n'
|
|
elif '\n' in contents:
|
|
line_break = '\n'
|
|
|
|
lines = contents.split(line_break)
|
|
for line in lines:
|
|
line = line.strip()
|
|
log.debug("STRM Line: {0}".format(line))
|
|
if line.startswith('#KODIPROP:'):
|
|
match = re.search('#KODIPROP:(?P<item_property>[^=]+?)=(?P<property_value>.+)', line)
|
|
if match:
|
|
item_property = match.group('item_property')
|
|
property_value = match.group('property_value')
|
|
log.debug("STRM property found: {0} value: {1}".format(item_property, property_value))
|
|
listitem_props.append((item_property, property_value))
|
|
else:
|
|
log.debug("STRM #KODIPROP incorrect format")
|
|
elif line.startswith('#'):
|
|
# unrecognized, treat as comment
|
|
log.debug("STRM unrecognized line identifier, ignored")
|
|
elif line != '':
|
|
playurl = line
|
|
log.debug("STRM playback url found")
|
|
|
|
log.debug("Playback URL: {0} ListItem Properties: {1}".format(playurl, listitem_props))
|
|
return playurl, listitem_props
|
|
|
|
|
|
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 = "/Sessions/Playing"
|
|
api.post(url, postdata)
|
|
|
|
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.lower() not in (
|
|
'plugin.video.jellycon', 'xbmc', 'upnextprovider.signal'
|
|
):
|
|
return
|
|
|
|
|
|
signal = method.split('.', 1)[-1]
|
|
if signal not in (
|
|
"jellycon_play_action", "jellycon_play_youtube_trailer_action",
|
|
"set_view", "plugin.video.jellycon_play_action"):
|
|
return
|
|
|
|
data_json = json.loads(data)
|
|
if sender.lower() == "upnextprovider.signal":
|
|
play_info = json.loads(binascii.unhexlify(data_json[0]))
|
|
else:
|
|
play_info = data_json[0]
|
|
|
|
log.debug("PlaybackService:onNotification:{0}".format(play_info))
|
|
|
|
if signal in (
|
|
"jellycon_play_action", "plugin.video.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")
|
|
|
|
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
|
|
|
|
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)")
|
|
|
|
|
|
def get_item_playback_info(item_id, force_transcode):
|
|
|
|
# Filter codecs that should NEVER be played directly (always force transcoding)
|
|
# These settings work independently from the target codec setting below
|
|
# Example: force_transcode_h264=true + target=hevc means: H.264 files will be transcoded to H.265
|
|
filtered_codecs = []
|
|
if settings.getSetting("force_transcode_h264") == "true":
|
|
filtered_codecs.append("h264")
|
|
if settings.getSetting("force_transcode_h265") == "true":
|
|
filtered_codecs.append("hevc")
|
|
filtered_codecs.append("h265")
|
|
if settings.getSetting("force_transcode_mpeg2") == "true":
|
|
filtered_codecs.append("mpeg2video")
|
|
if settings.getSetting("force_transcode_msmpeg4v3") == "true":
|
|
filtered_codecs.append("msmpeg4v3")
|
|
if settings.getSetting("force_transcode_mpeg4") == "true":
|
|
filtered_codecs.append("mpeg4")
|
|
if settings.getSetting("force_transcode_av1") == "true":
|
|
filtered_codecs.append("av1")
|
|
|
|
if not force_transcode:
|
|
bitrate = get_bitrate(settings.getSetting("max_stream_bitrate"))
|
|
else:
|
|
bitrate = get_bitrate(settings.getSetting("force_max_stream_bitrate"))
|
|
|
|
audio_codec = settings.getSetting("audio_codec")
|
|
audio_playback_bitrate = settings.getSetting("audio_playback_bitrate")
|
|
audio_max_channels = settings.getSetting("audio_max_channels")
|
|
|
|
audio_bitrate = int(audio_playback_bitrate) * 1000
|
|
|
|
# Determine target video codec for transcoding
|
|
# Note: force_transcode_* settings filter codecs for DirectPlay independently
|
|
# This setting only affects what codec the server transcodes TO when transcoding is needed
|
|
transcode_target_codec_setting = settings.getSetting("transcode_target_video_codec")
|
|
log.debug("Transcode target codec setting value: '{0}'".format(transcode_target_codec_setting))
|
|
if transcode_target_codec_setting == "1":
|
|
transcode_video_codec = "hevc"
|
|
elif transcode_target_codec_setting == "2":
|
|
transcode_video_codec = "av1"
|
|
else:
|
|
transcode_video_codec = "h264"
|
|
log.debug("Transcode target video codec: {0}".format(transcode_video_codec))
|
|
|
|
profile = {
|
|
"Name": "Kodi",
|
|
"MaxStaticBitrate": bitrate,
|
|
"MaxStreamingBitrate": bitrate,
|
|
"MusicStreamingTranscodingBitrate": audio_bitrate,
|
|
"TimelineOffsetSeconds": 5,
|
|
"TranscodingProfiles": [
|
|
{
|
|
"Type": "Audio"
|
|
},
|
|
{
|
|
"Container": "ts",
|
|
"Protocol": "hls",
|
|
"Type": "Video",
|
|
"AudioCodec": audio_codec,
|
|
"VideoCodec": transcode_video_codec,
|
|
"MaxAudioChannels": audio_max_channels
|
|
},
|
|
{
|
|
"Container": "jpeg",
|
|
"Type": "Photo"
|
|
}
|
|
],
|
|
"DirectPlayProfiles": [
|
|
{
|
|
"Type": "Video"
|
|
},
|
|
{
|
|
"Type": "Audio"
|
|
},
|
|
{
|
|
"Type": "Photo"
|
|
}
|
|
],
|
|
"ResponseProfiles": [],
|
|
"ContainerProfiles": [],
|
|
"CodecProfiles": [],
|
|
"SubtitleProfiles": [
|
|
{
|
|
"Format": "srt",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "srt",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "ass",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "ass",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "sub",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "sub",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "ssa",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "ssa",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "smi",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "smi",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "pgssub",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "pgssub",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "dvdsub",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "dvdsub",
|
|
"Method": "External"
|
|
},
|
|
{
|
|
"Format": "pgs",
|
|
"Method": "Embed"
|
|
},
|
|
{
|
|
"Format": "pgs",
|
|
"Method": "External"
|
|
}
|
|
]
|
|
}
|
|
|
|
if len(filtered_codecs) > 0:
|
|
profile['DirectPlayProfiles'][0]['VideoCodec'] = "-%s" % ",".join(filtered_codecs)
|
|
|
|
if force_transcode:
|
|
profile['DirectPlayProfiles'] = []
|
|
|
|
if settings.getSetting("playback_video_force_8") == "true":
|
|
profile['CodecProfiles'].append(
|
|
{
|
|
"Type": "Video",
|
|
"Codec": "h264",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitDepth",
|
|
"Value": "8",
|
|
"IsRequired": False
|
|
}
|
|
]
|
|
}
|
|
)
|
|
profile['CodecProfiles'].append(
|
|
{
|
|
"Type": "Video",
|
|
"Codec": "h265,hevc",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoProfile",
|
|
"Value": "main"
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
playback_info = {
|
|
'UserId': api.user_id,
|
|
'DeviceProfile': profile,
|
|
'AutoOpenLiveStream': True
|
|
}
|
|
|
|
if force_transcode:
|
|
url = "/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s&EnableDirectPlay=false&EnableDirectStream=false" % (item_id, bitrate)
|
|
else:
|
|
url = "/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s" % (item_id, bitrate)
|
|
|
|
log.debug("PlaybackInfo : {0}".format(url))
|
|
log.debug("PlaybackInfo : {0}".format(profile))
|
|
play_info_result = api.post(url, playback_info)
|
|
log.debug("PlaybackInfo : {0}".format(play_info_result))
|
|
|
|
return play_info_result
|
|
|
|
def get_media_segments(item_id):
|
|
url = "/MediaSegments/{}".format(item_id)
|
|
result = api.get(url)
|
|
if result is None or result["Items"] is None:
|
|
log.debug("GetMediaSegments : Media segments cloud not be retrieved")
|
|
return None
|
|
return result["Items"]
|