mirror of
https://github.com/jellyfin/jellyfin-mpv-shim.git
synced 2024-11-23 14:09:57 +00:00
Introduce the interactive menu, with the ability to adjust transcoding and bulk set subtitle/audio settings.
This commit is contained in:
parent
15fcb36e7c
commit
cf4c9db0a4
15
README.md
15
README.md
@ -64,6 +64,21 @@ is available under the terms of the MIT License. The project was ported
|
||||
to python3, modified to use mpv as the player, and updated to allow all
|
||||
features of the remote control api for video playback.
|
||||
|
||||
## Interactive Menu
|
||||
|
||||
Plex-MPV-Shim 1.3 introduces the interactive menu. This allows for features that
|
||||
would be impossible to support through the remote control interface normally, such as:
|
||||
- Change subtitle or audio tracks. The full titles from the media file are shown.
|
||||
- Automatically change the subtitle and audio streams for an entire series at once.
|
||||
- Adjust video transcoding quality without restarting.
|
||||
- Change transcoding preferences without editing the config file.
|
||||
- Quit the player, marking the current video unwatched.
|
||||
|
||||
The interactive menu supports both the keyboard and the remote control. To open the menu
|
||||
from a phone, press the ok or home button. Press back to go back or close the menu. The
|
||||
keyboard shortcut to open the menu is `c`. Use the arrow keys, escape, and enter/space
|
||||
to navigate the menu.
|
||||
|
||||
## Transcoding Support
|
||||
|
||||
Plex-MPV-Shim 1.2 introduces revamped transcoding support. It will automatically ask the server to see if transcoding is suggested, which enables Plex-MPV-Shim to play more of your library on the go. You can configure this or switch to the old local transcode decision system.
|
||||
|
218
plex_mpv_shim/bulk_subtitle.py
Normal file
218
plex_mpv_shim/bulk_subtitle.py
Normal file
@ -0,0 +1,218 @@
|
||||
from .media import XMLCollection
|
||||
from .utils import get_plex_url
|
||||
|
||||
from collections import namedtuple
|
||||
import urllib.parse
|
||||
import requests
|
||||
import time
|
||||
|
||||
Part = namedtuple("Part", ["id", "audio", "subtitle"])
|
||||
Audio = namedtuple("Audio", ["id", "language_code", "name", "plex_name"])
|
||||
Subtitle = namedtuple("Subtitle", ["id", "language_code", "name", "is_forced", "plex_name"])
|
||||
|
||||
messages = []
|
||||
keep_messages = 6
|
||||
|
||||
def render_message(message, show_text):
|
||||
messages.append(message)
|
||||
text = "Selecting Tracks..."
|
||||
for message in messages[-6:]:
|
||||
text += "\n " + message
|
||||
show_text(text,2**30,1)
|
||||
|
||||
def process_series(mode, url, player, m_raid=None, m_rsid=None):
|
||||
messages.clear()
|
||||
show_text = player._player.show_text
|
||||
c_aid, c_sid = None, None
|
||||
c_pid = player._video._part_node.get("id")
|
||||
|
||||
success_ct = 0
|
||||
partial_ct = 0
|
||||
count = 0
|
||||
|
||||
xml = XMLCollection(url)
|
||||
for video in xml.tree.findall("./Video"):
|
||||
name = "s{0}e{1:02}".format(int(video.get("parentIndex")), int(video.get("index")))
|
||||
video = XMLCollection(xml.get_path(video.get("key"))).tree.find("./")
|
||||
for partxml in video.findall("./Media/Part"):
|
||||
count += 1
|
||||
audio_list = [Audio(s.get("id"), s.get("languageCode"), s.get("title"),
|
||||
s.get("displayTitle")) for s in partxml.findall("./Stream[@streamType='2']")]
|
||||
subtitle_list = [Subtitle(s.get("id"), s.get("languageCode"), s.get("title"),
|
||||
"Forced" in s.get("displayTitle"), s.get("displayTitle"))
|
||||
for s in partxml.findall("./Stream[@streamType='3']")]
|
||||
part = Part(partxml.get("id"), audio_list, subtitle_list)
|
||||
|
||||
aid = None
|
||||
sid = "0"
|
||||
if mode == "subbed":
|
||||
audio, subtitle = get_subbed(part)
|
||||
if audio and subtitle:
|
||||
render_message("{0}: {1} ({2})".format(
|
||||
name, subtitle.plex_name, subtitle.name), show_text)
|
||||
aid, sid = audio.id, subtitle.id
|
||||
success_ct += 1
|
||||
elif mode == "dubbed":
|
||||
audio, subtitle = get_dubbed(part)
|
||||
if audio and subtitle:
|
||||
render_message("{0}: {1} ({2})".format(
|
||||
name, subtitle.plex_name, subtitle.name), show_text)
|
||||
aid, sid = audio.id, subtitle.id
|
||||
success_ct += 1
|
||||
elif audio:
|
||||
render_message("{0}: No Subtitles".format(name), show_text)
|
||||
aid = audio.id
|
||||
partial_ct += 1
|
||||
elif mode == "manual":
|
||||
if m_raid < len(part.audio) and m_rsid < len(part.subtitle):
|
||||
audio = part.audio[m_raid]
|
||||
aid = audio.id
|
||||
render_message("{0} a: {1} ({2})".format(
|
||||
name, audio.plex_name, audio.name), show_text)
|
||||
if m_rsid != -1:
|
||||
subtitle = part.subtitle[m_rsid]
|
||||
sid = subtitle.id
|
||||
render_message("{0} s: {1} ({2})".format(
|
||||
name, subtitle.plex_name, subtitle.name), show_text)
|
||||
success_ct += 1
|
||||
|
||||
if aid:
|
||||
if c_pid == part.id:
|
||||
c_aid, c_sid = aid, sid
|
||||
|
||||
args = {
|
||||
"allParts": "1",
|
||||
"audioStreamID": aid,
|
||||
"subtitleStreamID": sid
|
||||
}
|
||||
url = "/library/parts/{0}".format(part.id)
|
||||
requests.put(get_plex_url(urllib.parse.urljoin(xml.server_url, url), args), data=None)
|
||||
else:
|
||||
render_message("{0}: Fail".format(name), show_text)
|
||||
|
||||
if mode == "subbed":
|
||||
render_message("Set Subbed: {0} ok, {1} fail".format(
|
||||
success_ct, count-success_ct), show_text)
|
||||
elif mode == "dubbed":
|
||||
render_message("Set Dubbed: {0} ok, {1} audio only, {2} fail".format(
|
||||
success_ct, partial_ct, count-success_ct-partial_ct), show_text)
|
||||
elif mode == "manual":
|
||||
render_message("Manual: {0} ok, {1} fail".format(
|
||||
success_ct, count-success_ct), show_text)
|
||||
time.sleep(3)
|
||||
if c_aid:
|
||||
render_message("Setting Current...", show_text)
|
||||
if player._video.is_transcode:
|
||||
player.put_task(player.set_streams, c_aid, c_sid)
|
||||
player.timeline_handle()
|
||||
else:
|
||||
player.set_streams(c_aid, c_sid)
|
||||
|
||||
def get_subbed(part):
|
||||
japanese_audio = None
|
||||
english_subtitles = None
|
||||
subtitle_weight = None
|
||||
|
||||
for audio in part.audio:
|
||||
lower_title = audio.name.lower() if audio.name is not None else ""
|
||||
if audio.language_code != "jpn" and not "japan" in lower_title:
|
||||
continue
|
||||
if "commentary" in lower_title:
|
||||
continue
|
||||
|
||||
if japanese_audio is None:
|
||||
japanese_audio = audio
|
||||
break
|
||||
|
||||
for subtitle in part.subtitle:
|
||||
lower_title = subtitle.name.lower() if subtitle.name is not None else ""
|
||||
if subtitle.language_code != "eng" and not "english" in lower_title:
|
||||
continue
|
||||
if subtitle.is_forced:
|
||||
continue
|
||||
|
||||
weight = dialogue_weight(lower_title)
|
||||
if subtitle_weight is None or weight < subtitle_weight:
|
||||
subtitle_weight = weight
|
||||
english_subtitles = subtitle
|
||||
|
||||
if japanese_audio and english_subtitles:
|
||||
return japanese_audio, english_subtitles
|
||||
return None, None
|
||||
|
||||
def get_dubbed(part):
|
||||
english_audio = None
|
||||
sign_subtitles = None
|
||||
subtitle_weight = None
|
||||
|
||||
for audio in part.audio:
|
||||
lower_title = audio.name.lower() if audio.name is not None else ""
|
||||
if audio.language_code != "eng" and not "english" in lower_title:
|
||||
continue
|
||||
if "commentary" in lower_title:
|
||||
continue
|
||||
|
||||
if english_audio is None:
|
||||
english_audio = audio
|
||||
break
|
||||
|
||||
for subtitle in part.subtitle:
|
||||
lower_title = subtitle.name.lower() if subtitle.name is not None else ""
|
||||
if subtitle.language_code != "eng" and not "english" in lower_title:
|
||||
continue
|
||||
if subtitle.is_forced:
|
||||
sign_subtitles = subtitle
|
||||
break
|
||||
|
||||
weight = sign_weight(lower_title)
|
||||
if weight == 0:
|
||||
continue
|
||||
|
||||
if subtitle_weight is None or weight < subtitle_weight:
|
||||
subtitle_weight = weight
|
||||
sign_subtitles = subtitle
|
||||
|
||||
if english_audio:
|
||||
return english_audio, sign_subtitles
|
||||
return None, None
|
||||
|
||||
def dialogue_weight(text):
|
||||
if not text:
|
||||
return 900
|
||||
lower_text = text.lower()
|
||||
has_dialogue = "main" in lower_text or "full" in lower_text or "dialogue" in lower_text
|
||||
has_songs = "op/ed" in lower_text or "song" in lower_text or "lyric" in lower_text
|
||||
has_signs = "sign" in lower_text
|
||||
vendor = "bd" in lower_text or "retail" in lower_text
|
||||
weight = 900
|
||||
|
||||
if has_dialogue and has_songs:
|
||||
weight -= 100
|
||||
if has_songs:
|
||||
weight += 200
|
||||
if has_dialogue and has_signs:
|
||||
weight -= 100
|
||||
elif has_signs:
|
||||
weight += 700
|
||||
if vendor:
|
||||
weight += 50
|
||||
return weight
|
||||
|
||||
def sign_weight(text):
|
||||
if not text:
|
||||
return 0
|
||||
lower_text = text.lower()
|
||||
has_songs = "op/ed" in lower_text or "song" in lower_text or "lyric" in lower_text
|
||||
has_signs = "sign" in lower_text
|
||||
vendor = "bd" in lower_text or "retail" in lower_text
|
||||
weight = 900
|
||||
|
||||
if not (has_songs or has_signs):
|
||||
return 0
|
||||
if has_songs:
|
||||
weight -= 200
|
||||
if has_signs:
|
||||
weight -= 300
|
||||
if vendor:
|
||||
weight += 50
|
||||
return weight
|
@ -31,6 +31,16 @@ log = logging.getLogger("client")
|
||||
|
||||
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
||||
|
||||
NAVIGATION_DICT = {
|
||||
"/player/navigation/moveDown": "down",
|
||||
"/player/navigation/moveUp": "up",
|
||||
"/player/navigation/select": "ok",
|
||||
"/player/navigation/moveLeft": "left",
|
||||
"/player/navigation/moveRight": "right",
|
||||
"/player/navigation/home": "home",
|
||||
"/player/navigation/back": "back"
|
||||
}
|
||||
|
||||
class HttpHandler(SimpleHTTPRequestHandler):
|
||||
xmlOutput = None
|
||||
completed = False
|
||||
@ -349,8 +359,10 @@ class HttpHandler(SimpleHTTPRequestHandler):
|
||||
def mirror(self, path, arguments):
|
||||
timelineManager.delay_idle()
|
||||
|
||||
def navigation(self, path, query):
|
||||
pass
|
||||
def navigation(self, path, arguments):
|
||||
path = path.path
|
||||
if path in NAVIGATION_DICT:
|
||||
playerManager.menu_action(NAVIGATION_DICT[path])
|
||||
|
||||
class HttpSocketServer(ThreadingMixIn, HTTPServer):
|
||||
allow_reuse_address = True
|
||||
|
@ -35,6 +35,7 @@ class Video(object):
|
||||
self.is_transcode = False
|
||||
self.trs_aid = None
|
||||
self.trs_sid = None
|
||||
self.trs_ovr = None
|
||||
|
||||
if media:
|
||||
self.select_media(media, part)
|
||||
@ -158,6 +159,26 @@ class Video(object):
|
||||
setattr(self, "_title", title)
|
||||
return getattr(self, "_title")
|
||||
|
||||
def set_trs_override(self, video_bitrate, force_transcode, force_bitrate):
|
||||
if force_transcode:
|
||||
self.trs_ovr = (video_bitrate, force_transcode, force_bitrate)
|
||||
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 is_local_domain(self.parent.path.hostname):
|
||||
return "max"
|
||||
else:
|
||||
return settings.transcode_kbps
|
||||
|
||||
|
||||
def get_formats(self):
|
||||
audio_formats = []
|
||||
protocols = "protocols=http-video,http-live-streaming,http-mp4-streaming,http-mp4-video,http-mp4-video-720p,http-streaming-video,http-streaming-video-720p;videoDecoders=mpeg4,h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac{channels:8}"
|
||||
@ -208,8 +229,8 @@ class Video(object):
|
||||
"copyts": "1",
|
||||
}
|
||||
|
||||
if not is_local or force_bitrate:
|
||||
args[maxVideoBitrate] = str(video_bitrate)
|
||||
if video_bitrate is not None and (not is_local or force_bitrate):
|
||||
args["maxVideoBitrate"] = str(video_bitrate)
|
||||
|
||||
if audio_formats:
|
||||
args["X-Plex-Client-Profile-Extra"] = "+".join(audio_formats)
|
||||
@ -238,8 +259,10 @@ class Video(object):
|
||||
Returns the URL to use for the trancoded file.
|
||||
"""
|
||||
reset_session(self.parent.path.hostname)
|
||||
|
||||
if video_bitrate is None:
|
||||
|
||||
if self.trs_ovr:
|
||||
video_bitrate, force_transcode, force_bitrate = self.trs_ovr
|
||||
elif video_bitrate is None:
|
||||
video_bitrate = settings.transcode_kbps
|
||||
|
||||
if direct_play is None:
|
||||
@ -275,8 +298,8 @@ class Video(object):
|
||||
#"skipSubtitles": "1",
|
||||
}
|
||||
|
||||
if not is_local or force_bitrate:
|
||||
args[maxVideoBitrate] = str(video_bitrate)
|
||||
if video_bitrate is not None and (not is_local or force_bitrate):
|
||||
args["maxVideoBitrate"] = str(video_bitrate)
|
||||
|
||||
audio_formats, protocols = self.get_formats()
|
||||
|
||||
|
@ -5,16 +5,35 @@ import requests
|
||||
import urllib.parse
|
||||
|
||||
from threading import RLock
|
||||
from queue import Queue
|
||||
from queue import Queue, LifoQueue
|
||||
|
||||
from . import conffile
|
||||
from .utils import synchronous, Timer, get_plex_url
|
||||
from .conf import settings
|
||||
from .bulk_subtitle import process_series
|
||||
|
||||
APP_NAME = 'plex-mpv-shim'
|
||||
|
||||
TRANSCODE_LEVELS = (
|
||||
("1080p 20 Mbps", 20000),
|
||||
("1080p 12 Mbps", 12000),
|
||||
("1080p 10 Mbps", 10000),
|
||||
("720p 4 Mbps", 4000),
|
||||
("720p 3 Mbps", 3000),
|
||||
("720p 2 Mbps", 2000),
|
||||
("480p 1.5 Mbps", 1500),
|
||||
("328p 0.7 Mbps", 720),
|
||||
("240p 0.3 Mbps", 320),
|
||||
("160p 0.2 Mbps", 208),
|
||||
)
|
||||
|
||||
log = logging.getLogger('player')
|
||||
|
||||
# Q: What is with the put_task and timeline_handle?
|
||||
# A: If something that modifies the url is called from a keybind
|
||||
# directly, it crashes the input handling. If you know why,
|
||||
# please tell me. I'd love to get rid of it.
|
||||
|
||||
class PlayerManager(object):
|
||||
"""
|
||||
Manages the relationship between a ``Player`` instance and a ``Media``
|
||||
@ -28,6 +47,13 @@ class PlayerManager(object):
|
||||
mpv_config = conffile.get(APP_NAME,"mpv.conf", True)
|
||||
self._player = mpv.MPV(input_default_bindings=True, input_vo_keyboard=True, include=mpv_config)
|
||||
self.timeline_trigger = None
|
||||
self.is_menu_shown = False
|
||||
self.menu_title = ""
|
||||
self.menu_stack = LifoQueue()
|
||||
self.menu_list = []
|
||||
self.menu_selection = 0
|
||||
self.menu_tmp = None
|
||||
|
||||
if hasattr(self._player, 'osc'):
|
||||
self._player.osc = True
|
||||
else:
|
||||
@ -61,10 +87,62 @@ class PlayerManager(object):
|
||||
self.put_task(self.unwatched_quit)
|
||||
self.timeline_handle()
|
||||
|
||||
@self._player.on_key_press('c')
|
||||
def menu_open():
|
||||
if not self.is_menu_shown:
|
||||
self.show_menu()
|
||||
else:
|
||||
self.hide_menu()
|
||||
|
||||
@self._player.on_key_press('esc')
|
||||
def menu_back():
|
||||
self.menu_action('back')
|
||||
|
||||
@self._player.on_key_press('enter')
|
||||
def menu_ok():
|
||||
self.menu_action('ok')
|
||||
|
||||
@self._player.on_key_press('left')
|
||||
def menu_left():
|
||||
if self.is_menu_shown:
|
||||
self.menu_action('left')
|
||||
else:
|
||||
self._player.command("seek", -5)
|
||||
|
||||
@self._player.on_key_press('right')
|
||||
def menu_right():
|
||||
if self.is_menu_shown:
|
||||
self.menu_action('right')
|
||||
else:
|
||||
self._player.command("seek", 5)
|
||||
|
||||
@self._player.on_key_press('up')
|
||||
def menu_up():
|
||||
if self.is_menu_shown:
|
||||
self.menu_action('up')
|
||||
else:
|
||||
self._player.command("seek", 60)
|
||||
|
||||
@self._player.on_key_press('down')
|
||||
def menu_down():
|
||||
if self.is_menu_shown:
|
||||
self.menu_action('down')
|
||||
else:
|
||||
self._player.command("seek", -60)
|
||||
|
||||
@self._player.on_key_press('space')
|
||||
def handle_unwatched():
|
||||
self.toggle_pause()
|
||||
self.timeline_handle()
|
||||
def handle_pause():
|
||||
if self.is_menu_shown:
|
||||
self.menu_action('ok')
|
||||
else:
|
||||
self.toggle_pause()
|
||||
self.timeline_handle()
|
||||
|
||||
# This gives you an interactive python debugger prompt.
|
||||
@self._player.on_key_press('~')
|
||||
def handle_debug():
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
|
||||
@self._player.event_callback('idle')
|
||||
def handle_end(event):
|
||||
@ -78,6 +156,267 @@ class PlayerManager(object):
|
||||
|
||||
self.__part = 1
|
||||
|
||||
# The menu is a bit of a hack...
|
||||
# It works using multiline OSD.
|
||||
# We also have to force the window to open.
|
||||
|
||||
def refresh_menu(self):
|
||||
if not self.is_menu_shown:
|
||||
return
|
||||
|
||||
items = self.menu_list
|
||||
selected_item = self.menu_selection
|
||||
|
||||
menu_text = "{0}".format(self.menu_title)
|
||||
for i, item in enumerate(items):
|
||||
fmt = "\n {0}"
|
||||
if i == selected_item:
|
||||
fmt = "\n **{0}**"
|
||||
menu_text += fmt.format(item[0])
|
||||
|
||||
self._player.show_text(menu_text,2**30,1)
|
||||
|
||||
def show_menu(self):
|
||||
self.is_menu_shown = True
|
||||
self._player.osd_back_color = '#CC333333'
|
||||
self._player.osd_font_size = 40
|
||||
|
||||
if hasattr(self._player, 'osc'):
|
||||
self._player.osc = False
|
||||
|
||||
if self._player.playback_abort:
|
||||
self._player.force_window = True
|
||||
self._player.keep_open = True
|
||||
self._player.play("")
|
||||
self._player.fs = True
|
||||
else:
|
||||
self._player.pause = True
|
||||
|
||||
self.menu_title = "Main Menu"
|
||||
self.menu_selection = 0
|
||||
|
||||
if self._video and not self._player.playback_abort:
|
||||
self.menu_list = [
|
||||
("Change Audio", self.change_audio_menu),
|
||||
("Change Subtitles", self.change_subtitle_menu),
|
||||
("Change Video Quality", self.change_transcode_quality),
|
||||
("Auto Set Audio/Subtitles (Entire Series)", self.change_tracks_menu),
|
||||
("Quit and Mark Unwatched", self.unwatched_menu_handle),
|
||||
]
|
||||
else:
|
||||
self.menu_list = []
|
||||
|
||||
self.menu_list.extend([
|
||||
("Preferences", self.preferences_menu),
|
||||
("Close Menu", self.hide_menu)
|
||||
])
|
||||
|
||||
self.put_task(self.unhide_menu)
|
||||
self.refresh_menu()
|
||||
|
||||
def hide_menu(self):
|
||||
if self.is_menu_shown:
|
||||
self._player.osd_back_color = '#00000000'
|
||||
self._player.osd_font_size = 55
|
||||
self._player.show_text("",0,0)
|
||||
self._player.force_window = False
|
||||
self._player.keep_open = False
|
||||
|
||||
if hasattr(self._player, 'osc'):
|
||||
self._player.osc = True
|
||||
|
||||
if self._player.playback_abort:
|
||||
self._player.play("")
|
||||
else:
|
||||
self._player.pause = False
|
||||
|
||||
self.is_menu_shown = False
|
||||
|
||||
def menu_action(self, action):
|
||||
if not self.is_menu_shown and action in ("home", "ok"):
|
||||
self.show_menu()
|
||||
else:
|
||||
if action == "up":
|
||||
self.menu_selection = (self.menu_selection - 1) % len(self.menu_list)
|
||||
elif action == "down":
|
||||
self.menu_selection = (self.menu_selection + 1) % len(self.menu_list)
|
||||
elif action == "back":
|
||||
if self.menu_stack.empty():
|
||||
self.hide_menu()
|
||||
else:
|
||||
self.menu_title, self.menu_list, self.menu_selection = self.menu_stack.get_nowait()
|
||||
elif action == "ok":
|
||||
self.menu_list[self.menu_selection][1]()
|
||||
elif action == "home":
|
||||
self.show_menu()
|
||||
self.refresh_menu()
|
||||
|
||||
def change_audio_menu_handle(self):
|
||||
if self._video.is_transcode:
|
||||
self.put_task(self.set_streams, self.menu_list[self.menu_selection][2], None)
|
||||
self.timeline_handle()
|
||||
else:
|
||||
self.set_streams(self.menu_list[self.menu_selection][2], None)
|
||||
self.menu_action("back")
|
||||
|
||||
def change_audio_menu(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Select Audio Track"
|
||||
self.menu_list = []
|
||||
self.menu_selection = 0
|
||||
|
||||
selected_aid, _ = self.get_track_ids()
|
||||
audio_streams = playerManager._video._part_node.findall("./Stream[@streamType='2']")
|
||||
for i, audio_track in enumerate(audio_streams):
|
||||
aid = audio_track.get("id")
|
||||
self.menu_list.append([
|
||||
"{0} ({1})".format(audio_track.get("displayTitle"), audio_track.get("title")),
|
||||
self.change_audio_menu_handle,
|
||||
aid
|
||||
])
|
||||
if aid == selected_aid:
|
||||
self.menu_selection = i
|
||||
|
||||
def change_subtitle_menu_handle(self):
|
||||
if self._video.is_transcode:
|
||||
self.put_task(self.set_streams, None, self.menu_list[self.menu_selection][2])
|
||||
self.timeline_handle()
|
||||
else:
|
||||
self.set_streams(None, self.menu_list[self.menu_selection][2])
|
||||
self.menu_action("back")
|
||||
|
||||
def change_subtitle_menu(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Select Subtitle Track"
|
||||
self.menu_list = []
|
||||
self.menu_selection = 0
|
||||
|
||||
_, selected_sid = self.get_track_ids()
|
||||
subtitle_streams = playerManager._video._part_node.findall("./Stream[@streamType='3']")
|
||||
self.menu_list.append(["None", self.change_subtitle_menu_handle, "0"])
|
||||
for i, subtitle_track in enumerate(subtitle_streams):
|
||||
sid = subtitle_track.get("id")
|
||||
self.menu_list.append([
|
||||
"{0} ({1})".format(subtitle_track.get("displayTitle"), subtitle_track.get("title")),
|
||||
self.change_subtitle_menu_handle,
|
||||
sid
|
||||
])
|
||||
if sid == selected_sid:
|
||||
self.menu_selection = i+1
|
||||
|
||||
def change_transcode_quality_handle(self):
|
||||
bitrate = self.menu_list[self.menu_selection][2]
|
||||
if bitrate == "none":
|
||||
self._video.set_trs_override(None, False, False)
|
||||
elif bitrate == "max":
|
||||
self._video.set_trs_override(None, True, False)
|
||||
else:
|
||||
self._video.set_trs_override(bitrate, True, True)
|
||||
|
||||
self.menu_action("back")
|
||||
self.put_task(self.restart_playback)
|
||||
self.timeline_handle()
|
||||
|
||||
def change_transcode_quality(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Select Transcode Quality"
|
||||
handle = self.change_transcode_quality_handle
|
||||
self.menu_list = [
|
||||
("No Transcode", handle, "none"),
|
||||
("Maximum", handle, "max")
|
||||
]
|
||||
|
||||
for item in TRANSCODE_LEVELS:
|
||||
self.menu_list.append((item[0], handle, item[1]))
|
||||
|
||||
self.menu_selection = 7
|
||||
cur_bitrate = self._video.get_transcode_bitrate()
|
||||
for i, option in enumerate(self.menu_list):
|
||||
if cur_bitrate == option[2]:
|
||||
self.menu_selection = i
|
||||
|
||||
def change_tracks_handle(self):
|
||||
mode = self.menu_list[self.menu_selection][2]
|
||||
parentSeriesKey = self._video.parent.tree.find("./").get("parentKey") + "/children"
|
||||
url = self._video.parent.get_path(parentSeriesKey)
|
||||
process_series(mode, url, self)
|
||||
|
||||
def change_tracks_manual_s1(self):
|
||||
self.change_audio_menu()
|
||||
for item in self.menu_list:
|
||||
item[1] = self.change_tracks_manual_s2
|
||||
|
||||
def change_tracks_manual_s2(self):
|
||||
self.menu_tmp = self.menu_selection
|
||||
self.change_subtitle_menu()
|
||||
for item in self.menu_list:
|
||||
item[1] = self.change_tracks_manual_s3
|
||||
|
||||
def change_tracks_manual_s3(self):
|
||||
aid, sid = self.menu_tmp, self.menu_selection - 1
|
||||
# Pop 3 menu items.
|
||||
for i in range(3):
|
||||
self.menu_action("back")
|
||||
parentSeriesKey = self._video.parent.tree.find("./").get("parentKey") + "/children"
|
||||
url = self._video.parent.get_path(parentSeriesKey)
|
||||
process_series("manual", url, self, aid, sid)
|
||||
|
||||
def change_tracks_menu(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Select Audio/Subtitle for Series"
|
||||
self.menu_selection = 0
|
||||
self.menu_list = [
|
||||
("English Audio", self.change_tracks_handle, "dubbed"),
|
||||
("Japanese Audio w/ English Subtitles", self.change_tracks_handle, "subbed"),
|
||||
("Manual by Track Index (Less Reliable)", self.change_tracks_manual_s1),
|
||||
]
|
||||
|
||||
def settings_toggle_bool(self):
|
||||
_, _, key, name = self.menu_list[self.menu_selection]
|
||||
setattr(settings, key, not getattr(settings, key))
|
||||
settings.save()
|
||||
self.menu_list[self.menu_selection] = self.get_settings_toggle(name, key)
|
||||
|
||||
def get_settings_toggle(self, name, setting):
|
||||
return (
|
||||
"{0}: {1}".format(name, getattr(settings, setting)),
|
||||
self.settings_toggle_bool,
|
||||
setting,
|
||||
name
|
||||
)
|
||||
|
||||
def transcode_settings_handle(self):
|
||||
settings.transcode_kbps = self.menu_list[self.menu_selection][2]
|
||||
settings.save()
|
||||
|
||||
# Need to re-render preferences menu.
|
||||
for i in range(2):
|
||||
self.menu_action("back")
|
||||
self.preferences_menu()
|
||||
|
||||
def transcode_settings_menu(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Select Default Transcode Profile"
|
||||
self.menu_selection = 0
|
||||
self.menu_list = []
|
||||
handle = self.transcode_settings_handle
|
||||
|
||||
for i, item in enumerate(TRANSCODE_LEVELS):
|
||||
self.menu_list.append((item[0], handle, item[1]))
|
||||
if settings.transcode_kbps == item[1]:
|
||||
self.menu_selection = i
|
||||
|
||||
def preferences_menu(self):
|
||||
self.menu_stack.put((self.menu_title, self.menu_list, self.menu_selection))
|
||||
self.menu_title = "Preferences"
|
||||
self.menu_selection = 0
|
||||
self.menu_list = [
|
||||
self.get_settings_toggle("Adaptive Transcode", "adaptive_transcode"),
|
||||
self.get_settings_toggle("Always Transcode", "always_transcode"),
|
||||
self.get_settings_toggle("Auto Play", "auto_play"),
|
||||
("Transcode Quality: {0:0.1f} Mbps".format(settings.transcode_kbps/1000), self.transcode_settings_menu)
|
||||
]
|
||||
|
||||
def put_task(self, func, *args):
|
||||
self.evt_queue.put([func, args])
|
||||
|
||||
@ -85,6 +424,15 @@ class PlayerManager(object):
|
||||
if self.timeline_trigger:
|
||||
self.timeline_trigger.set()
|
||||
|
||||
def unwatched_menu_handle(self):
|
||||
self.put_task(self.unwatched_quit)
|
||||
self.timeline_handle()
|
||||
|
||||
def unhide_menu(self):
|
||||
# Sometimes, mpv completely ignores the OSD text.
|
||||
# Setting this value usually causes it to appear...
|
||||
self._player.osd_align_x = 'left'
|
||||
|
||||
@synchronous('_lock')
|
||||
def update(self):
|
||||
while not self.evt_queue.empty():
|
||||
@ -105,6 +453,7 @@ class PlayerManager(object):
|
||||
@synchronous('_lock')
|
||||
def _play_media(self, video, url, offset=0):
|
||||
self.url = url
|
||||
self.hide_menu()
|
||||
|
||||
self._player.play(self.url)
|
||||
self._player.wait_for_property("duration")
|
||||
@ -280,6 +629,18 @@ class PlayerManager(object):
|
||||
|
||||
if self._video.is_transcode:
|
||||
self.restart_playback()
|
||||
|
||||
def get_track_ids(self):
|
||||
if self._video.is_transcode:
|
||||
return self._video.get_transcode_streams()
|
||||
else:
|
||||
aid, sid = None, None
|
||||
if self._player.sub != 'no':
|
||||
sid = self._video.subtitle_uid.get(self._player.sub, '')
|
||||
|
||||
if self._player.audio != 'no':
|
||||
aid = self._video.audio_uid.get(self._player.audio, '')
|
||||
return aid, sid
|
||||
|
||||
playerManager = PlayerManager()
|
||||
|
||||
|
@ -153,18 +153,12 @@ class TimelineManager(threading.Thread):
|
||||
options["time"] = int(player.playback_time * 1e3)
|
||||
options["autoPlay"] = '1' if settings.auto_play else '0'
|
||||
|
||||
if video.is_transcode:
|
||||
trs_audio, trs_subtitle = video.get_transcode_streams()
|
||||
if trs_subtitle:
|
||||
options["subtitleStreamID"] = trs_subtitle
|
||||
if trs_audio:
|
||||
options["audioStreamID"] = trs_audio
|
||||
else:
|
||||
if player.sub != 'no':
|
||||
options["subtitleStreamID"] = video.subtitle_uid.get(player.sub, '')
|
||||
aid, sid = playerManager.get_track_ids()
|
||||
|
||||
if player.audio != 'no':
|
||||
options["audioStreamID"] = video.audio_uid.get(player.audio, '')
|
||||
if aid:
|
||||
options["audioStreamID"] = aid
|
||||
if sid:
|
||||
options["subtitleStreamID"] = sid
|
||||
|
||||
options["ratingKey"] = video.get_video_attr("ratingKey")
|
||||
options["key"] = video.get_video_attr("key")
|
||||
|
Loading…
Reference in New Issue
Block a user