mirror of
https://github.com/jellyfin/jellyfin-mpv-shim.git
synced 2025-02-24 08:30:47 +00:00
297 lines
12 KiB
Python
297 lines
12 KiB
Python
import logging
|
|
import uuid
|
|
import urllib.parse
|
|
import os.path
|
|
import re
|
|
import pathlib
|
|
from sys import platform
|
|
|
|
from .conf import settings
|
|
from .utils import is_local_domain, get_profile, get_seq
|
|
from .i18n import _
|
|
|
|
log = logging.getLogger('media')
|
|
|
|
class Video(object):
|
|
def __init__(self, item_id, parent, aid=None, sid=None, srcid=None):
|
|
self.item_id = item_id
|
|
self.parent = parent
|
|
self.client = parent.client
|
|
self.aid = aid
|
|
self.sid = sid
|
|
self.item = self.client.jellyfin.get_item(item_id)
|
|
|
|
self.is_tv = self.item.get("Type") == "Episode"
|
|
|
|
self.subtitle_seq = {}
|
|
self.subtitle_uid = {}
|
|
self.subtitle_url = {}
|
|
self.subtitle_enc = set()
|
|
self.audio_seq = {}
|
|
self.audio_uid = {}
|
|
self.is_transcode = False
|
|
self.trs_ovr = None
|
|
self.playback_info = None
|
|
self.media_source = None
|
|
self.srcid = srcid
|
|
|
|
def map_streams(self):
|
|
self.subtitle_seq = {}
|
|
self.subtitle_uid = {}
|
|
self.subtitle_url = {}
|
|
self.subtitle_enc = set()
|
|
self.audio_seq = {}
|
|
self.audio_uid = {}
|
|
|
|
if self.media_source is None or self.media_source["Protocol"] != "File":
|
|
return
|
|
|
|
index = 1
|
|
for stream in self.media_source["MediaStreams"]:
|
|
if stream.get("Type") != "Audio":
|
|
continue
|
|
|
|
self.audio_uid[index] = stream["Index"]
|
|
self.audio_seq[stream["Index"]] = index
|
|
|
|
if stream.get("IsExternal") == False:
|
|
index += 1
|
|
|
|
index = 1
|
|
for sub in self.media_source["MediaStreams"]:
|
|
if sub.get("Type") != "Subtitle":
|
|
continue
|
|
|
|
if sub.get("DeliveryMethod") == "Embed":
|
|
self.subtitle_uid[index] = sub["Index"]
|
|
self.subtitle_seq[sub["Index"]] = index
|
|
elif sub.get("DeliveryMethod") == "External":
|
|
url = sub.get("DeliveryUrl")
|
|
if not sub.get("IsExternalUrl"):
|
|
url = self.client.config.data["auth.server"] + url
|
|
self.subtitle_url[sub["Index"]] = url
|
|
elif sub.get("DeliveryMethod") == "Encode":
|
|
self.subtitle_enc.add(sub["Index"])
|
|
|
|
if sub.get("IsExternal") == False:
|
|
index += 1
|
|
|
|
user_aid = self.media_source.get("DefaultAudioStreamIndex")
|
|
user_sid = self.media_source.get("DefaultSubtitleStreamIndex")
|
|
|
|
if user_aid is not None and self.aid is None:
|
|
self.aid = user_aid
|
|
|
|
if user_sid is not None and self.sid is None:
|
|
self.sid = user_sid
|
|
|
|
def get_current_streams(self):
|
|
return self.aid, self.sid
|
|
|
|
def get_proper_title(self):
|
|
if not hasattr(self, "_title"):
|
|
title = self.item.get("Name")
|
|
if (self.is_tv and self.item.get("IndexNumber") is not None
|
|
and self.item.get("ParentIndexNumber") is not None):
|
|
episode_number = int(self.item.get("IndexNumber"))
|
|
season_number = int(self.item.get("ParentIndexNumber"))
|
|
series_name = self.item.get("SeriesName")
|
|
title = "%s - s%de%.2d - %s" % (series_name, season_number, episode_number, title)
|
|
elif self.item.get("Type") == "Movie":
|
|
year = self.item.get("ProductionYear")
|
|
if year is not None:
|
|
title = "%s (%s)" % (title, year)
|
|
setattr(self, "_title", title)
|
|
return getattr(self, "_title") + (_(" (Transcode)") if self.is_transcode else "")
|
|
|
|
def set_trs_override(self, video_bitrate, force_transcode):
|
|
if force_transcode:
|
|
self.trs_ovr = (video_bitrate, force_transcode)
|
|
else:
|
|
self.trs_ovr = None
|
|
|
|
def get_transcode_bitrate(self):
|
|
if not self.is_transcode:
|
|
return "none"
|
|
elif self.trs_ovr is not None:
|
|
if self.trs_ovr[0] is not None:
|
|
return self.trs_ovr[0]
|
|
elif self.trs_ovr[1]:
|
|
return "max"
|
|
elif self.parent.is_local:
|
|
return "max"
|
|
else:
|
|
return settings.remote_kbps
|
|
|
|
def terminate_transcode(self):
|
|
if self.is_transcode:
|
|
self.client.jellyfin.close_transcode(self.client.config.data["app.device_id"])
|
|
|
|
def _get_url_from_source(self, source):
|
|
# Only use Direct Paths if:
|
|
# - The media source supports direct paths.
|
|
# - Direct paths are enabled in the config.
|
|
# - The server is local or the override config is set.
|
|
# - If there's a scheme specified or the path exists as a local file.
|
|
if ((self.media_source.get('Protocol') == "Http" or self.media_source['SupportsDirectPlay'])
|
|
and settings.direct_paths and (settings.remote_direct_paths or self.parent.is_local)):
|
|
|
|
if platform.startswith("win32") or platform.startswith("cygwin"):
|
|
# matches on SMB scheme
|
|
match = re.search('(?:\\\\).+:.*@(.+)', self.media_source['Path'])
|
|
if match:
|
|
# replace forward slash to backward slashes
|
|
log.debug("cleaned up credentials from path")
|
|
self.media_source['Path'] = str(pathlib.Path('\\\\' + match.groups()[0]))
|
|
|
|
if urllib.parse.urlparse(self.media_source['Path']).scheme:
|
|
self.is_transcode = False
|
|
log.debug("Using remote direct path.")
|
|
# translate path for windows
|
|
# if path is smb path in credential format for kodi and maybe linux \\username:password@mediaserver\foo,
|
|
# translate it to mediaserver/foo
|
|
return pathlib.Path(self.media_source['Path'])
|
|
else:
|
|
# If there's no uri scheme, check if the file exixsts because it might not be mounted
|
|
if os.path.isfile(self.media_source['Path']):
|
|
log.debug("Using local direct path.")
|
|
self.is_transcode = False
|
|
return self.media_source['Path']
|
|
|
|
if self.media_source['SupportsDirectStream']:
|
|
self.is_transcode = False
|
|
log.debug("Using direct url.")
|
|
return "%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" % (
|
|
self.client.config.data["auth.server"],
|
|
self.item_id,
|
|
self.media_source['Id'],
|
|
self.client.config.data["auth.token"]
|
|
)
|
|
elif self.media_source['SupportsTranscoding']:
|
|
log.debug("Using transcode url.")
|
|
self.is_transcode = True
|
|
return self.client.config.data["auth.server"] + self.media_source.get("TranscodingUrl")
|
|
|
|
def get_best_media_source(self, preferred=None):
|
|
weight_selected = 0
|
|
preferred_selected = None
|
|
selected = None
|
|
for media_source in self.playback_info["MediaSources"]:
|
|
if media_source.get("Id") == preferred:
|
|
preferred_selected = media_source
|
|
# Prefer the highest bitrate file that will direct play.
|
|
weight = (media_source.get("SupportsDirectPlay") or 0) * 50000 + (media_source.get("Bitrate") or 0) / 1000
|
|
if weight > weight_selected:
|
|
weight_selected = weight
|
|
selected = media_source
|
|
if preferred_selected:
|
|
return preferred_selected
|
|
else:
|
|
if preferred is not None:
|
|
log.warning("Preferred media source is unplayable.")
|
|
return selected
|
|
|
|
def get_playback_url(self, offset=0, video_bitrate=None, force_transcode=False, force_bitrate=False):
|
|
"""
|
|
Returns the URL to use for the trancoded file.
|
|
"""
|
|
self.terminate_transcode()
|
|
|
|
if self.trs_ovr:
|
|
video_bitrate, force_transcode = self.trs_ovr
|
|
|
|
log.debug("Bandwidth: local={0}, bitrate={1}, force={2}".format(self.parent.is_local, video_bitrate, force_transcode))
|
|
profile = get_profile(not self.parent.is_local, video_bitrate, force_transcode)
|
|
self.playback_info = self.client.jellyfin.get_play_info(self.item_id, profile, self.aid, self.sid)
|
|
|
|
self.media_source = self.get_best_media_source(self.srcid)
|
|
self.map_streams()
|
|
url = self._get_url_from_source(self.media_source)
|
|
|
|
# If there are more media sources and the default one fails, try all of them.
|
|
if url is None and len(self.playback_info["MediaSources"]) > 1:
|
|
log.warning("Selected media source is unplayable.")
|
|
for media_source in self.playback_info["MediaSources"]:
|
|
if media_source["Id"] != self.srcid:
|
|
self.media_source = media_source
|
|
self.map_streams()
|
|
url = self._get_url_from_source(self.media_source)
|
|
if url is not None:
|
|
break
|
|
|
|
if settings.log_decisions:
|
|
if len(self.playback_info["MediaSources"]) > 1:
|
|
log.debug("Full Playback Info: {0}".format(self.playback_info))
|
|
log.debug("Media Decision: {0}".format(self.media_source))
|
|
return url
|
|
|
|
def get_duration(self):
|
|
ticks = self.item.get("RunTimeTicks")
|
|
if ticks:
|
|
return ticks / 10000000
|
|
|
|
def set_played(self, watched=True):
|
|
self.client.jellyfin.item_played(self.item_id, watched)
|
|
|
|
def set_streams(self, aid, sid):
|
|
need_restart = False
|
|
|
|
if aid is not None and self.aid != aid:
|
|
self.aid = aid
|
|
if self.is_transcode:
|
|
need_restart = True
|
|
|
|
if sid is not None and self.sid != sid:
|
|
self.sid = sid
|
|
if sid in self.subtitle_enc:
|
|
need_restart = True
|
|
|
|
return need_restart
|
|
|
|
class Media(object):
|
|
def __init__(self, client, queue, seq=0, user_id=None, aid=None, sid=None, srcid=None, queue_override=True):
|
|
if queue_override:
|
|
self.queue = [{ "PlaylistItemId": "playlistItem{0}".format(get_seq()), "Id": id_num } for id_num in queue]
|
|
else:
|
|
self.queue = queue
|
|
self.client = client
|
|
self.seq = seq
|
|
self.user_id = user_id
|
|
|
|
self.video = Video(self.queue[seq]["Id"], self, aid, sid, srcid)
|
|
self.is_tv = self.video.is_tv
|
|
self.is_local = is_local_domain(client)
|
|
self.has_next = seq < len(queue) - 1
|
|
self.has_prev = seq > 0
|
|
|
|
def get_next(self):
|
|
if self.has_next:
|
|
return Media(self.client, self.queue, self.seq+1, self.user_id, queue_override=False)
|
|
|
|
def get_prev(self):
|
|
if self.has_prev:
|
|
return Media(self.client, self.queue, self.seq-1, self.user_id, queue_override=False)
|
|
|
|
def get_from_key(self, item_id):
|
|
for i, video in enumerate(self.queue):
|
|
if video["Id"] == item_id:
|
|
return Media(self.client, self.queue, i, self.user_id, queue_override=False)
|
|
return None
|
|
|
|
def get_video(self, index):
|
|
if index == 0 and self.video:
|
|
return self.video
|
|
|
|
if index < len(self.queue):
|
|
return Video(self.queue[index]["Id"], self)
|
|
|
|
log.error("Media::get_video couldn't find video at index %s" % index)
|
|
|
|
def insert_items(self, items, append=False):
|
|
items = [{ "PlaylistItemId": "playlistItem{0}".format(get_seq()), "Id": id_num } for id_num in items]
|
|
if append:
|
|
self.queue.extend(items)
|
|
else:
|
|
self.queue = self.queue[0:self.seq+1] + items + self.queue[self.seq+1:]
|
|
self.has_next = self.seq < len(self.queue) - 1
|