├── jellyfin_mpv_shim ├── __init__.py ├── systray.png ├── integration │ ├── jellyfin-16.png │ ├── jellyfin-32.png │ ├── jellyfin-48.png │ ├── jellyfin-64.png │ ├── jellyfin-128.png │ ├── jellyfin-256.png │ ├── com.github.iwalton3.jellyfin-mpv-shim.desktop │ └── com.github.iwalton3.jellyfin-mpv-shim.appdata.xml ├── cli_mgr.py ├── constants.py ├── action_thread.py ├── rich_presence.py ├── i18n.py ├── mouse.lua ├── win_utils.py ├── conffile.py ├── timeline.py ├── log_utils.py ├── settings_base.py ├── update_check.py ├── mpv_shim.py ├── display_mirror │ ├── __init__.py │ ├── index.html │ └── helpers.py ├── svp_integration.py ├── event_handler.py ├── conf.py ├── video_profile.py ├── utils.py ├── clients.py ├── bulk_subtitle.py └── messages │ ├── am │ └── LC_MESSAGES │ │ └── base.po │ ├── base.pot │ ├── be │ └── LC_MESSAGES │ │ └── base.po │ ├── bn │ └── LC_MESSAGES │ │ └── base.po │ ├── eo │ └── LC_MESSAGES │ │ └── base.po │ └── fi │ └── LC_MESSAGES │ └── base.po ├── jellyfin.ico ├── jellyfin.icns ├── .idea ├── misc.xml ├── vcs.xml ├── .gitignore ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml └── jellyfin-mpv-shim.iml ├── MANIFEST.in ├── .gitignore ├── regen_pot.sh ├── get_pywebview_natives.py ├── setup-mac.py ├── artifacts.sh ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── run.py ├── hidpi.manifest ├── setup.py ├── Jellyfin MPV Shim.iss ├── gen_pkg.sh └── CONTRIBUTING.md /jellyfin_mpv_shim/__init__.py: -------------------------------------------------------------------------------- 1 | # There is nothing here. 2 | -------------------------------------------------------------------------------- /jellyfin.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin.ico -------------------------------------------------------------------------------- /jellyfin.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin.icns -------------------------------------------------------------------------------- /jellyfin_mpv_shim/systray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/systray.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-16.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-32.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-48.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-64.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-128.png -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/jellyfin-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Terminus-Media/jellyfin-mpv-shim/HEAD/jellyfin_mpv_shim/integration/jellyfin-256.png -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include jellyfin_mpv_shim/systray.png 2 | recursive-include jellyfin_mpv_shim/integration * 3 | recursive-include jellyfin_mpv_shim/default_shader_pack * 4 | recursive-include jellyfin_mpv_shim/messages *.mo 5 | include jellyfin_mpv_shim/mouse.lua 6 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/com.github.iwalton3.jellyfin-mpv-shim.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jellyfin MPV Shim 3 | Exec=jellyfin-mpv-shim 4 | Type=Application 5 | Icon=com.github.iwalton3.jellyfin-mpv-shim 6 | Categories=Video;AudioVideo;TV;Player 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | dist 3 | build 4 | jellyfin_mpv_shim.egg-info 5 | webclient 6 | cefpython3 7 | *.dll 8 | *.spec 9 | *.pyc 10 | mpv32 11 | .eggs 12 | jellyfin_mpv_shim/default_shader_pack/* 13 | *.mo 14 | .last_sp_version 15 | .last_wc_version 16 | debug.log 17 | publish 18 | -------------------------------------------------------------------------------- /regen_pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pygettext --default-domain=base -o jellyfin_mpv_shim/messages/base.pot jellyfin_mpv_shim/*.py jellyfin_mpv_shim/**/*.py 3 | find -iname '*.po' | while read -r file 4 | do 5 | msgmerge --update "$file" --backup=none --previous jellyfin_mpv_shim/messages/base.pot 6 | done 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /get_pywebview_natives.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import webview 3 | import os.path 4 | import shutil 5 | 6 | lib_root = os.path.join(os.path.dirname(webview.__file__), "lib") 7 | 8 | files_to_copy = [ 9 | "WebBrowserInterop.x86.dll", 10 | "WebBrowserInterop.x64.dll", 11 | "Microsoft.Toolkit.Forms.UI.Controls.WebView.dll" 12 | ] 13 | 14 | for f in files_to_copy: 15 | shutil.copy(os.path.join(lib_root, f), ".") 16 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/cli_mgr.py: -------------------------------------------------------------------------------- 1 | from .clients import clientManager 2 | 3 | 4 | class UserInterface(object): 5 | def __init__(self): 6 | self.open_player_menu = lambda: None 7 | self.stop = lambda: None 8 | 9 | @staticmethod 10 | def login_servers(): 11 | clientManager.cli_connect() 12 | 13 | def start(self): 14 | pass 15 | 16 | def stop(self): 17 | pass 18 | 19 | 20 | user_interface = UserInterface() 21 | -------------------------------------------------------------------------------- /setup-mac.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | APP = ["/usr/local/bin/jellyfin-mpv-shim"] 4 | OPTIONS = { 5 | "argv_emulation": True, 6 | "iconfile": "jellyfin.icns", 7 | "resources": [ 8 | "/usr/local/bin/mpv", 9 | "/usr/local/bin/jellyfin-mpv-shim" 10 | ], 11 | "packages": ["pkg_resources"], 12 | } 13 | 14 | setup( 15 | app=APP, 16 | name="Jellyfin MPV Shim", 17 | options={"py2app": OPTIONS}, 18 | setup_requires=["py2app"], 19 | ) 20 | -------------------------------------------------------------------------------- /artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p publish publish/Installer publish/InstallerLegacy publish/Debug 3 | version=$(cat jellyfin_mpv_shim/constants.py | grep '^CLIENT_VERSION' | cut -d '"' -f 2) 4 | if [[ "$1" == "standard" ]] 5 | then 6 | cp dist/jellyfin-mpv-shim_version_installer.exe publish/Installer/jellyfin-mpv-shim_${version}_installer.exe || exit 1 7 | #mv dist/run publish/Debug/ || exit 1 8 | elif [[ "$1" == "legacy" ]] 9 | then 10 | cp dist/jellyfin-mpv-shim_version_installer.exe publish/InstallerLegacy/jellyfin-mpv-shim_${version}_LEGACY32_installer.exe || exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/constants.py: -------------------------------------------------------------------------------- 1 | APP_NAME = "jellyfin-mpv-shim" 2 | USER_APP_NAME = "Jellyfin MPV Shim" 3 | CLIENT_VERSION = "2.0.1" 4 | USER_AGENT = "Jellyfin-MPV-Shim/%s" % CLIENT_VERSION 5 | CAPABILITIES = { 6 | "PlayableMediaTypes": "Video", 7 | "SupportsMediaControl": True, 8 | "SupportedCommands": ( 9 | "MoveUp,MoveDown,MoveLeft,MoveRight,Select," 10 | "Back,ToggleFullscreen," 11 | "GoHome,GoToSettings,TakeScreenshot," 12 | "VolumeUp,VolumeDown,ToggleMute," 13 | "SetAudioStreamIndex,SetSubtitleStreamIndex," 14 | "Mute,Unmute,SetVolume,DisplayContent," 15 | "Play,Playstate,PlayNext,PlayMediaSource" 16 | ), 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/action_thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from .player import playerManager 4 | 5 | 6 | class ActionThread(threading.Thread): 7 | def __init__(self): 8 | self.trigger = threading.Event() 9 | self.halt = False 10 | 11 | threading.Thread.__init__(self) 12 | 13 | def stop(self): 14 | self.halt = True 15 | self.join() 16 | 17 | def run(self): 18 | force_next = False 19 | while not self.halt: 20 | if playerManager.is_active() or force_next: 21 | playerManager.update() 22 | 23 | force_next = False 24 | if self.trigger.wait(1): 25 | force_next = True 26 | self.trigger.clear() 27 | 28 | 29 | actionThread = ActionThread() 30 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Newer revisions of python-mpv require mpv-1.dll in the PATH. 4 | import os 5 | import sys 6 | import multiprocessing 7 | 8 | if sys.platform.startswith("win32") or sys.platform.startswith("cygwin"): 9 | # Detect if bundled via pyinstaller. 10 | # From: https://stackoverflow.com/questions/404744/ 11 | if getattr(sys, "frozen", False): 12 | application_path = getattr(sys, "_MEIPASS") 13 | else: 14 | application_path = os.path.dirname(os.path.abspath(__file__)) 15 | os.environ["PATH"] = application_path + os.pathsep + os.environ["PATH"] 16 | 17 | from jellyfin_mpv_shim.mpv_shim import main 18 | 19 | if __name__ == "__main__": 20 | # https://stackoverflow.com/questions/24944558/pyinstaller-built-windows-exe-fails-with-multiprocessing 21 | multiprocessing.freeze_support() 22 | 23 | main() 24 | -------------------------------------------------------------------------------- /.idea/jellyfin-mpv-shim.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/rich_presence.py: -------------------------------------------------------------------------------- 1 | from pypresence import Presence 2 | import time 3 | 4 | client_id = "743296148592263240" 5 | RPC = Presence(client_id) 6 | RPC.connect() 7 | 8 | 9 | def send_presence( 10 | title: str, 11 | subtitle: str, 12 | playback_time: float = None, 13 | duration: float = None, 14 | playing: bool = False, 15 | ): 16 | small_image = "play-dark3" if playing else None 17 | start = None 18 | end = None 19 | if playback_time is not None and duration is not None and playing: 20 | start = int(time.time() - playback_time) 21 | end = int(start + duration) 22 | RPC.update( 23 | state=subtitle, 24 | details=title, 25 | instance=False, 26 | large_image="jellyfin2", 27 | start=start, 28 | end=end, 29 | large_text="Jellyfin", 30 | small_image=small_image, 31 | ) 32 | 33 | 34 | def clear_presence(): 35 | RPC.clear() 36 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/i18n.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import locale 3 | 4 | from .conf import settings 5 | 6 | translation = gettext.NullTranslations() 7 | 8 | 9 | def configure(): 10 | global translation 11 | from .utils import get_resource 12 | 13 | messages_dir = get_resource("messages") 14 | lang = None 15 | 16 | if settings.lang is not None: 17 | lang = settings.lang 18 | else: 19 | # This is more robust than the built-in language detection in gettext. 20 | # Specifically, it supports Windows correctly. 21 | lc = locale.getdefaultlocale() 22 | if lc is not None and lc[0] is not None: 23 | lang = lc[0] 24 | 25 | if lang is not None: 26 | translation = gettext.translation( 27 | "base", messages_dir, languages=[lang], fallback=True 28 | ) 29 | else: 30 | translation = gettext.translation("base", messages_dir, fallback=True) 31 | 32 | 33 | def get_translation(): 34 | return translation 35 | 36 | 37 | def _(string: str) -> str: 38 | return translation.gettext(string) 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to improve the project 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | Attention! Please be sure to avoid posting any API keys. I don't want other people to be able to compromise your server! 14 | API Keys are now removed from logs by default, but they may still appear in other places, such as on MPV info screens. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. Windows] 31 | - Version [e.g. 1.2.0] 32 | 33 | **Error Messages** 34 | Please provide logs, as they are often needed for me to understand and quickly troubleshoot the issue. You can read instructions for how to do so here: 35 | https://github.com/iwalton3/jellyfin-mpv-shim/wiki/Sending-Logs 36 | -------------------------------------------------------------------------------- /hidpi.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PerMonitorV2 21 | 22 | 23 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/mouse.lua: -------------------------------------------------------------------------------- 1 | last_idx = -1 2 | function mouse_handler() 3 | local x, y = mp.get_mouse_pos() 4 | local hy = mp.get_property_native("osd-height") 5 | if hy == nil 6 | then 7 | return 8 | end 9 | idx = math.floor((y * 1000 / hy - 33) / 55) 10 | if idx ~= last_idx 11 | then 12 | last_idx = idx 13 | mp.commandv("script-message", "shim-menu-select", idx) 14 | end 15 | end 16 | 17 | function mouse_click_handler() 18 | last_idx = -1 -- Force refresh. 19 | mouse_handler() 20 | mp.commandv("script-message", "shim-menu-click") 21 | end 22 | 23 | function client_message_handler(event) 24 | if event["args"][1] == "shim-menu-enable" 25 | then 26 | if event["args"][2] == "True" 27 | then 28 | mp.log("info", "Enabled shim menu mouse events.") 29 | mp.add_key_binding("MOUSE_BTN0", "shim_mouse_click_handler", mouse_click_handler) 30 | mp.add_key_binding("MOUSE_MOVE", "shim_mouse_move_handler", mouse_handler) 31 | else 32 | mp.log("info", "Disabled shim menu mouse events.") 33 | mp.remove_key_binding("shim_mouse_click_handler") 34 | mp.remove_key_binding("shim_mouse_move_handler") 35 | end 36 | end 37 | end 38 | 39 | mp.add_key_binding("MOUSE_MOVE", "shim_mouse_move_handler", mouse_handler) 40 | mp.register_event("client-message", client_message_handler) 41 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/win_utils.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences,PyPackageRequirements 2 | import win32gui 3 | import logging 4 | 5 | log = logging.getLogger("win_utils") 6 | 7 | 8 | def window_enumeration_handler(hwnd, top_windows): 9 | top_windows.append((hwnd, win32gui.GetWindowText(hwnd))) 10 | 11 | 12 | def raise_mpv(): 13 | # This workaround is madness. Apparently SetForegroundWindow 14 | # won't work randomly, so I have to call ShowWindow twice. 15 | # Once to hide the window, and again to successfully raise the window. 16 | try: 17 | top_windows = [] 18 | fg_win = win32gui.GetForegroundWindow() 19 | win32gui.EnumWindows(window_enumeration_handler, top_windows) 20 | for i in top_windows: 21 | if " - mpv" in i[1].lower(): 22 | if i[0] != fg_win: 23 | win32gui.ShowWindow(i[0], 6) # Minimize 24 | win32gui.ShowWindow(i[0], 9) # Un-minimize 25 | break 26 | 27 | except Exception: 28 | log.error("Could not raise MPV.", exc_info=True) 29 | 30 | 31 | def mirror_act(state: bool, name: str = "Jellyfin MPV Shim Mirror"): 32 | try: 33 | top_windows = [] 34 | win32gui.EnumWindows(window_enumeration_handler, top_windows) 35 | for i in top_windows: 36 | if name in i[1]: 37 | print(i) 38 | win32gui.ShowWindow(i[0], 9 if state else 6) 39 | break 40 | 41 | except Exception: 42 | log.error("Could not raise/lower MPV mirror.", exc_info=True) 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="jellyfin-mpv-shim", 8 | version="2.0.1", 9 | author="Ian Walton", 10 | author_email="iwalton3@gmail.com", 11 | description="Cast media from Jellyfin Mobile and Web apps to MPV.", 12 | license="GPLv3", 13 | long_description=open("README.md").read(), 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/jellyfin/jellyfin-mpv-shim", 16 | packages=[ 17 | "jellyfin_mpv_shim", 18 | "jellyfin_mpv_shim.display_mirror" 19 | ], 20 | package_data={ 21 | "jellyfin_mpv_shim.display_mirror": ["*.css", "*.html"], 22 | "jellyfin_mpv_shim": ["systray.png"], 23 | }, 24 | entry_points={ 25 | "console_scripts": [ 26 | "jellyfin-mpv-shim=jellyfin_mpv_shim.mpv_shim:main" 27 | ] 28 | }, 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 32 | "Operating System :: OS Independent", 33 | ], 34 | extras_require={ 35 | "gui": ["pystray", "PIL"], 36 | "mirror": ["Jinja2", "pywebview>=3.3.1"], 37 | "discord": ["pypresence"], 38 | "all": [ 39 | "Jinja2", 40 | "pywebview>=3.3.1", 41 | "pystray", 42 | "pypresence", 43 | ], 44 | }, 45 | python_requires=">=3.6", 46 | install_requires=[ 47 | "python-mpv", 48 | "jellyfin-apiclient-python>=1.7.2", 49 | "python-mpv-jsonipc>=1.1.9", 50 | "requests", 51 | ], 52 | include_package_data=True, 53 | ) 54 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/conffile.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import os 3 | import sys 4 | import getpass 5 | 6 | # If no platform is matched, use the current directory. 7 | _confdir = None 8 | username = getpass.getuser() 9 | 10 | 11 | def posix(app: str): 12 | if os.environ.get("XDG_CONFIG_HOME"): 13 | return os.path.join(os.environ["XDG_CONFIG_HOME"], app) 14 | else: 15 | return os.path.join(os.path.expanduser("~"), ".config", app) 16 | 17 | 18 | def win32(app: str): 19 | if os.environ.get("APPDATA"): 20 | return os.path.join(os.environ["APPDATA"], app) 21 | else: 22 | return os.path.join(r"C:\Users", username, r"AppData\Roaming", app) 23 | 24 | 25 | confdirs = ( 26 | ("linux", posix), 27 | ("win32", win32), 28 | ("cygwin", posix), 29 | ( 30 | "darwin", 31 | lambda app: os.path.join( 32 | "/Users", username, "Library/Application Support", app 33 | ), 34 | ), 35 | ) 36 | 37 | for platform, directory in confdirs: 38 | if sys.platform.startswith(platform): 39 | _confdir = directory 40 | 41 | custom_config = None 42 | for i, arg in enumerate(sys.argv): 43 | if arg == "--config" and len(sys.argv) > i + 1: 44 | custom_config = sys.argv[i + 1] 45 | 46 | 47 | def confdir(app: str): 48 | if custom_config is not None: 49 | return custom_config 50 | elif _confdir is not None: 51 | return _confdir(app) 52 | else: 53 | return "" 54 | 55 | 56 | def get(app: str, conf_file: str, create: bool = False): 57 | conf_folder = confdir(app) 58 | if not os.path.isdir(conf_folder): 59 | os.makedirs(conf_folder) 60 | conf_file = os.path.join(conf_folder, conf_file) 61 | if create and not os.path.isfile(conf_file): 62 | open(conf_file, "w").close() 63 | return conf_file 64 | -------------------------------------------------------------------------------- /Jellyfin MPV Shim.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Jellyfin MPV Shim" 5 | #define MyAppVersion "2.0.1" 6 | #define MyAppPublisher "Ian Walton" 7 | #define MyAppURL "https://github.com/jellyfin/jellyfin-mpv-shim" 8 | #define MyAppExeName "run.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{898D84F4-BAD5-4014-B224-6536BFAE0A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={autopf}\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | LicenseFile=LICENSE.md 24 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 25 | ;PrivilegesRequired=lowest 26 | PrivilegesRequiredOverridesAllowed=dialog 27 | OutputDir=dist 28 | OutputBaseFilename=jellyfin-mpv-shim_version_installer 29 | SetupIconFile=jellyfin.ico 30 | Compression=lzma 31 | SolidCompression=yes 32 | WizardStyle=modern 33 | 34 | [Languages] 35 | Name: "english"; MessagesFile: "compiler:Default.isl" 36 | 37 | [Tasks] 38 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 39 | 40 | [Files] 41 | Source: "dist\run\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 42 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 43 | 44 | [Icons] 45 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 46 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 47 | 48 | [Run] 49 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 50 | 51 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/timeline.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import os 4 | 5 | import jellyfin_apiclient_python.exceptions 6 | 7 | from .conf import settings 8 | from .player import playerManager 9 | from .utils import Timer 10 | 11 | log = logging.getLogger("timeline") 12 | 13 | 14 | class TimelineManager(threading.Thread): 15 | def __init__(self): 16 | self.idleTimer = Timer() 17 | self.halt = False 18 | self.trigger = threading.Event() 19 | self.is_idle = True 20 | 21 | threading.Thread.__init__(self) 22 | 23 | def stop(self): 24 | self.halt = True 25 | self.join() 26 | 27 | def run(self): 28 | while not self.halt: 29 | if playerManager.is_active() and ( 30 | not settings.idle_when_paused or not playerManager.is_paused() 31 | ): 32 | self.send_timeline() 33 | self.delay_idle() 34 | if self.idleTimer.elapsed() > settings.idle_cmd_delay and not self.is_idle: 35 | if ( 36 | settings.idle_when_paused 37 | and settings.stop_idle 38 | and playerManager.has_video() 39 | ): 40 | playerManager.stop() 41 | if settings.idle_cmd: 42 | os.system(settings.idle_cmd) 43 | self.is_idle = True 44 | if self.trigger.wait(5): 45 | self.trigger.clear() 46 | 47 | def delay_idle(self): 48 | self.idleTimer.restart() 49 | self.is_idle = False 50 | 51 | @staticmethod 52 | def send_timeline(): 53 | try: 54 | # Send_timeline sometimes (once every couple hours) gets a 404 response from Jellyfin. 55 | # Without this try/except that would cause this entire thread to crash keeping it from self-healing. 56 | playerManager.send_timeline() 57 | except jellyfin_apiclient_python.exceptions.HTTPException: 58 | # FIXME: Log this 59 | pass 60 | 61 | 62 | timelineManager = TimelineManager() 63 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/log_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | bad_patterns = ( 5 | (re.compile("api_key=[a-f0-9]*"), "api_key=REDACTED"), 6 | ( 7 | re.compile("'X-MediaBrowser-Token': '[a-f0-9]*'"), 8 | "'X-MediaBrowser-Token': 'REDACTED'", 9 | ), 10 | (re.compile("'AccessToken': '[a-f0-9]*'"), "'AccessToken': 'REDACTED'"), 11 | ) 12 | 13 | sanitize_logs = False 14 | root_logger = logging.getLogger("") 15 | root_logger.level = logging.DEBUG 16 | 17 | 18 | def sanitize(message): 19 | if type(message) in (int, float): 20 | return message 21 | if message is not str and message is not bytes: 22 | message = str(message) 23 | for pattern, replacement in bad_patterns: 24 | message = pattern.sub(replacement, message) 25 | return message 26 | 27 | 28 | class CustomFormatter(logging.Formatter): 29 | def __init__(self, force_sanitize=False): 30 | self.force_sanitize = force_sanitize 31 | super(CustomFormatter, self).__init__( 32 | fmt="%(asctime)s [%(levelname)8s] %(name)s: %(message)s" 33 | ) 34 | 35 | def format(self, record): 36 | if sanitize_logs or self.force_sanitize: 37 | record.msg = sanitize(record.msg) 38 | if type(record.args) is dict: 39 | sanitized = {} 40 | for key, value in record.args.items(): 41 | sanitized[key] = sanitize(value) 42 | record.args = sanitized 43 | elif type(record.args) is tuple: 44 | record.args = tuple(sanitize(value) for value in record.args) 45 | else: 46 | return "(Log message could not be processed for sanitization.)" 47 | return logging.Formatter.format(self, record) 48 | 49 | 50 | def enable_sanitization(): 51 | global sanitize_logs 52 | sanitize_logs = True 53 | 54 | 55 | def configure_log(destination): 56 | handler = logging.StreamHandler(destination) 57 | handler.setFormatter(CustomFormatter()) 58 | root_logger.addHandler(handler) 59 | 60 | 61 | def configure_log_file(destination: str): 62 | handler = logging.FileHandler(destination, mode="w") 63 | # Never allow logging API keys to a file. 64 | handler.setFormatter(CustomFormatter(True)) 65 | root_logger.addHandler(handler) 66 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/settings_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Optional 3 | import logging 4 | log = logging.getLogger("settings_base") 5 | 6 | # This is NOT a full pydantic replacement!!! 7 | # Compatible with PEP 563 8 | # Tries to also deal with common errors in the config 9 | 10 | def allow_none(constructor): 11 | def wrapper(input): 12 | if input is None or input == "null": 13 | return None 14 | return constructor(input) 15 | return wrapper 16 | 17 | yes_set = {1, "yes", "Yes", "True", "true", "1", True} 18 | 19 | def adv_bool(value): 20 | return value in yes_set 21 | 22 | object_types = { 23 | "float": float, 24 | "int": int, 25 | "str": str, 26 | "bool": adv_bool, 27 | "Optional[float]": allow_none(float), 28 | "Optional[int]": allow_none(int), 29 | "Optional[str]": allow_none(str), 30 | "Optional[bool]": allow_none(adv_bool), 31 | float: float, 32 | int: int, 33 | str: str, 34 | bool: adv_bool, 35 | Optional[float]: allow_none(float), 36 | Optional[int]: allow_none(int), 37 | Optional[str]: allow_none(str), 38 | Optional[bool]: allow_none(adv_bool), 39 | } 40 | 41 | class SettingsBase: 42 | def __init__(self): 43 | self.__fields_set__ = set() 44 | self.__fields__ = [] 45 | for attr in self.__class__.__annotations__.keys(): 46 | if attr.startswith("_"): 47 | continue 48 | 49 | self.__fields__.append(attr) 50 | setattr(self, attr, getattr(self.__class__, attr)) 51 | 52 | def dict(self): 53 | result = {} 54 | for attr in self.__fields__: 55 | result[attr] = getattr(self, attr) 56 | return result 57 | 58 | def parse_obj(self, object): 59 | new_obj = self.__class__() 60 | annotations = self.__class__.__annotations__ 61 | 62 | for attr in self.__fields__: 63 | if attr in object: 64 | parse = object_types[annotations[attr]] 65 | try: 66 | setattr(new_obj, attr, parse(object[attr])) 67 | new_obj.__fields_set__.add(attr) 68 | except: 69 | log.error("Setting {0} had invalid value {1}.".format(attr, object[attr]), exc_info=True) 70 | return new_obj 71 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | build-win64: 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | curl -L https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210404-git-dd86f19.7z/download > mpv.7z 19 | 7z x mpv.7z 20 | pip install .[all] pywebview==3.4 pywin32 21 | ./gen_pkg.sh --skip-build 22 | python ./get_pywebview_natives.py 23 | shell: bash 24 | - name: PyInstaller Bootloader 25 | run: | 26 | ./gen_pkg.sh --get-pyinstaller; cd pyinstaller/bootloader; python ./waf distclean all; cd ..; python setup.py install 27 | shell: bash 28 | - name: Main Build 29 | run: | 30 | .\build-win.bat 31 | shell: cmd 32 | - name: Artifact Rename 33 | run: | 34 | ./artifacts.sh standard 35 | shell: bash 36 | - name: Archive production artifacts 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: windows 40 | path: ${{ github.workspace }}/publish/Installer/*.exe 41 | build-win32: 42 | runs-on: windows-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Setup Python 3.7 46 | uses: actions/setup-python@v1 47 | with: 48 | python-version: 3.7 49 | - name: Install dependencies 50 | run: | 51 | curl -L https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210404-git-dd86f19.7z/download > mpv.7z 52 | 7z x mpv.7z 53 | pip install .[all] pywebview==3.4 pywin32 54 | ./gen_pkg.sh --skip-build 55 | python ./get_pywebview_natives.py 56 | shell: bash 57 | - name: PyInstaller Bootloader 58 | run: | 59 | ./gen_pkg.sh --get-pyinstaller; cd pyinstaller/bootloader; python ./waf distclean all; cd ..; python setup.py install 60 | shell: bash 61 | - name: Legacy Build 62 | run: | 63 | .\build-win-32.bat 64 | shell: cmd 65 | - name: Artifact Rename 66 | run: | 67 | ./artifacts.sh legacy 68 | shell: bash 69 | - name: Archive production artifacts 70 | uses: actions/upload-artifact@v2 71 | with: 72 | name: windows-legacy32 73 | path: ${{ github.workspace }}/publish/InstallerLegacy/*.exe -------------------------------------------------------------------------------- /jellyfin_mpv_shim/update_check.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import datetime 3 | import logging 4 | import webbrowser 5 | 6 | from .constants import CLIENT_VERSION 7 | from .conf import settings 8 | from .i18n import _ 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from .player import PlayerManager as PlayerManager_type 14 | 15 | log = logging.getLogger("update_check") 16 | 17 | release_url = "https://github.com/jellyfin/jellyfin-mpv-shim/releases/" 18 | release_urls = [ 19 | release_url, 20 | "https://github.com/jellyfin/jellyfin-mpv-shim/releases/", 21 | "https://github.com/iwalton3/jellyfin-mpv-shim/releases/" 22 | ] 23 | one_day = 86400 24 | 25 | 26 | class UpdateChecker: 27 | def __init__(self, player_manager: "PlayerManager_type"): 28 | self.playerManager = player_manager 29 | self.has_notified = False 30 | self.new_version = None 31 | self.last_check = None 32 | 33 | def _check_updates(self): 34 | log.info("Checking for updates...") 35 | for release_url in release_urls: 36 | try: 37 | response = requests.get( 38 | release_url + "latest", allow_redirects=False, timeout=(3, 10) 39 | ) 40 | if response.status_code != 302: 41 | log.warning("Release page returned bad status code.") 42 | continue 43 | if not response.headers["location"].startswith(release_url): 44 | log.warning("Release page does not start with release_url.") 45 | continue 46 | version = response.headers["location"][len(release_url) + 5 :] 47 | if CLIENT_VERSION != version: 48 | self.new_version = version 49 | break 50 | except Exception: 51 | log.error("Could not check for updates.", exc_info=True) 52 | return self.new_version is not None 53 | 54 | def check(self): 55 | if not settings.check_updates: 56 | return 57 | 58 | if ( 59 | self.last_check is not None 60 | and (datetime.datetime.utcnow() - self.last_check).total_seconds() < one_day 61 | ): 62 | log.info("Update check performed in last day. Skipping.") 63 | return 64 | 65 | self.last_check = datetime.datetime.utcnow() 66 | if self.new_version is not None or self._check_updates(): 67 | if not self.has_notified and settings.notify_updates: 68 | self.has_notified = True 69 | log.info("Update Available: {0}".format(self.new_version)) 70 | self.playerManager.show_text( 71 | _( 72 | "MPV Shim v{0} Update Available\nOpen menu (press c) for details." 73 | ).format(self.new_version), 74 | 5000, 75 | 1, 76 | ) 77 | 78 | def open(self): 79 | self.playerManager.set_fullscreen(False) 80 | try: 81 | webbrowser.open(release_url + "latest") 82 | except Exception: 83 | log.error("Could not open release URL.", exc_info=True) 84 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/mpv_shim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | import sys 5 | import multiprocessing 6 | from threading import Event 7 | 8 | from . import conffile 9 | from . import i18n 10 | from .conf import settings 11 | from .clients import clientManager 12 | from .constants import APP_NAME 13 | from .log_utils import configure_log, enable_sanitization, configure_log_file 14 | 15 | configure_log(sys.stdout) 16 | log = logging.getLogger("") 17 | logging.getLogger("requests").setLevel(logging.CRITICAL) 18 | 19 | 20 | def main(): 21 | conf_file = conffile.get(APP_NAME, "conf.json") 22 | load_success = settings.load(conf_file) 23 | i18n.configure() 24 | 25 | if settings.sanitize_output: 26 | enable_sanitization() 27 | 28 | if settings.write_logs: 29 | log_file = conffile.get(APP_NAME, "log.txt") 30 | configure_log_file(log_file) 31 | 32 | if sys.platform.startswith("darwin"): 33 | multiprocessing.set_start_method("forkserver") 34 | 35 | user_interface = None 36 | mirror = None 37 | use_gui = False 38 | gui_ready = None 39 | get_webview = lambda: None 40 | if settings.enable_gui: 41 | try: 42 | from .gui_mgr import user_interface 43 | 44 | use_gui = True 45 | gui_ready = Event() 46 | user_interface.gui_ready = gui_ready 47 | except Exception: 48 | log.warning( 49 | "Cannot load GUI. Falling back to command line interface.", 50 | exc_info=True, 51 | ) 52 | 53 | if settings.display_mirroring: 54 | try: 55 | from .display_mirror import mirror 56 | 57 | get_webview = mirror.get_webview 58 | except ImportError: 59 | mirror = None 60 | log.warning("Cannot load display mirror.", exc_info=True) 61 | 62 | if not user_interface: 63 | from .cli_mgr import user_interface 64 | 65 | from .player import playerManager 66 | from .action_thread import actionThread 67 | from .event_handler import eventHandler 68 | from .timeline import timelineManager 69 | 70 | clientManager.callback = eventHandler.handle_event 71 | timelineManager.start() 72 | playerManager.timeline_trigger = timelineManager.trigger 73 | actionThread.start() 74 | playerManager.action_trigger = actionThread.trigger 75 | playerManager.get_webview = get_webview 76 | user_interface.open_player_menu = playerManager.menu.show_menu 77 | eventHandler.mirror = mirror 78 | user_interface.start() 79 | user_interface.login_servers() 80 | 81 | if not load_success: 82 | log.error("Your configuration file is not valid JSON! It has been ignored!") 83 | log.info("Tip: Open the JSON file in VS Code to see what is wrong.") 84 | 85 | try: 86 | if mirror: 87 | user_interface.stop_callback = mirror.stop 88 | # If the webview runs before the systray icon, it fails. 89 | if use_gui: 90 | gui_ready.wait() 91 | mirror.run() 92 | else: 93 | halt = Event() 94 | user_interface.stop_callback = halt.set 95 | try: 96 | halt.wait() 97 | except KeyboardInterrupt: 98 | print("") 99 | log.info("Stopping services...") 100 | finally: 101 | playerManager.terminate() 102 | timelineManager.stop() 103 | actionThread.stop() 104 | clientManager.stop() 105 | user_interface.stop() 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /gen_pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script: 3 | # - Checks the current version 4 | # - Verifies all versions match and are newer 5 | # - Downloads/updates web client 6 | # - Download/updates default-shader-pack 7 | # - Generates locales 8 | # - Builds the python package 9 | 10 | cd "$(dirname "$0")" 11 | 12 | function download_compat { 13 | if [[ "$AZ_CACHE" != "" ]] 14 | then 15 | download_id=$(echo "$2" | md5sum | sed 's/ .*//g') 16 | if [[ -e "$AZ_CACHE/$3/$download_id" ]] 17 | then 18 | echo "Cache hit: $AZ_CACHE/$3/$download_id" 19 | cp "$AZ_CACHE/$3/$download_id" "$1" 20 | return 21 | elif [[ "$3" != "" ]] 22 | then 23 | rm -r "$AZ_CACHE/$3" 2> /dev/null 24 | fi 25 | fi 26 | if [[ "$(which wget 2>/dev/null)" != "" ]] 27 | then 28 | wget -qO "$1" "$2" 29 | else [[ "$(which curl)" != "" ]] 30 | curl -sL "$2" > "$1" 31 | fi 32 | if [[ "$AZ_CACHE" != "" ]] 33 | then 34 | echo "Saving to: $AZ_CACHE/$3/$download_id" 35 | mkdir -p "$AZ_CACHE/$3/" 36 | cp "$1" "$AZ_CACHE/$3/$download_id" 37 | fi 38 | } 39 | 40 | function get_resource_version { 41 | curl -s --head https://github.com/"$1"/releases/latest | \ 42 | grep -i '^location: ' | sed 's/.*tag\///g' | tr -d '\r' 43 | } 44 | 45 | if [[ "$1" == "--get-pyinstaller" ]] 46 | then 47 | echo "Downloading pyinstaller..." 48 | pi_version=$(get_resource_version pyinstaller/pyinstaller) 49 | download_compat release.zip "https://github.com/pyinstaller/pyinstaller/archive/$pi_version.zip" "pi" 50 | ( 51 | mkdir pyinstaller 52 | cd pyinstaller 53 | unzip ../release.zip > /dev/null && rm ../release.zip 54 | mv pyinstaller-*/* ./ 55 | rm -r pyinstaller-* 56 | ) 57 | exit 0 58 | elif [[ "$1" == "--gen-fingerprint" ]] 59 | then 60 | ( 61 | get_resource_version pyinstaller/pyinstaller 62 | get_resource_version iwalton3/default-shader-pack 63 | ) | tee az-cache-fingerprint.list 64 | exit 0 65 | fi 66 | 67 | # Verify versioning 68 | current_version=$(get_resource_version jellyfin/jellyfin-mpv-shim) 69 | current_version=${current_version:1} 70 | constants_version=$(cat jellyfin_mpv_shim/constants.py | grep '^CLIENT_VERSION' | cut -d '"' -f 2) 71 | setup_version=$(grep 'version=' setup.py | cut -d '"' -f 2) 72 | iss_version=$(grep '^#define MyAppVersion' "Jellyfin MPV Shim.iss" | cut -d '"' -f 2) 73 | appdata_version=$(grep 'release version="' jellyfin_mpv_shim/integration/com.github.iwalton3.jellyfin-mpv-shim.appdata.xml | \ 74 | head -n 1 | cut -d '"' -f 2) 75 | 76 | if [[ "$current_version" == "$constants_version" ]] 77 | then 78 | echo "Warning: This version matches the current published version." 79 | echo "If you are building a release, the publish will not succeed." 80 | fi 81 | 82 | if [[ "$constants_version" != "$setup_version" || "$setup_version" != "$iss_version" || "$iss_version" != "$appdata_version" ]] 83 | then 84 | echo "Error: The release does not have the same version numbers in all files!" 85 | echo "Please correct this before releasing!" 86 | echo "Constants: $constants_version, Setup: $setup_version, ISS: $iss_version, Flatpak: $appdata_version" 87 | fi 88 | 89 | # Generate translations 90 | find -iname '*.po' | while read -r file 91 | do 92 | msgfmt "$file" -o "${file%.*}.mo" 93 | done 94 | 95 | # Download default-shader-pack 96 | update_shader_pack="no" 97 | if [[ ! -e "jellyfin_mpv_shim/default_shader_pack" ]] 98 | then 99 | update_shader_pack="yes" 100 | elif [[ -e ".last_sp_version" ]] 101 | then 102 | if [[ "$(get_resource_version iwalton3/default-shader-pack)" != "$(cat .last_sp_version)" ]] 103 | then 104 | update_shader_pack="yes" 105 | fi 106 | fi 107 | 108 | if [[ "$update_shader_pack" == "yes" ]] 109 | then 110 | echo "Downloading shaders..." 111 | sp_version=$(get_resource_version iwalton3/default-shader-pack) 112 | download_compat release.zip "https://github.com/iwalton3/default-shader-pack/archive/$sp_version.zip" "sp" 113 | rm -r jellyfin_mpv_shim/default_shader_pack 2> /dev/null 114 | ( 115 | mkdir default_shader_pack 116 | cd default_shader_pack 117 | unzip ../release.zip > /dev/null && rm ../release.zip 118 | mv default-shader-pack-*/* ./ 119 | rm -r default-shader-pack-* 120 | ) 121 | mv default_shader_pack jellyfin_mpv_shim/ 122 | echo "$sp_version" > .last_sp_version 123 | fi 124 | 125 | # Generate package 126 | if [[ "$1" == "--install" ]] 127 | then 128 | if [[ "$(which sudo 2> /dev/null)" != "" && ! "$*" =~ "--local" ]] 129 | then 130 | sudo pip3 install .[all] 131 | else 132 | pip3 install .[all] 133 | fi 134 | 135 | elif [[ "$1" != "--skip-build" ]] 136 | then 137 | rm -r build/ dist/ .eggs 2> /dev/null 138 | mkdir build/ dist/ 139 | echo "Building release package." 140 | python3 setup.py sdist bdist_wheel > /dev/null 141 | fi 142 | 143 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/display_mirror/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import jinja2 # python3-jinja2 in Debian, Jinja2 in pypi 4 | 5 | # So, most of my use of the webview library is ugly but there's a reason for this! 6 | # Debian's python3-webview package is super old (2.3), pip3's pywebview package is much newer (3.2). 7 | # I would just say "fuck it, install from testing/backports/unstable" except that's not any newer either. 8 | # So instead I'm going to try supporting *both* here, which gets rather ugly because they made very significant changes. 9 | # 10 | # The key differences seem to be: 11 | # 3.2's create_window() returns a Window object immediately, then you need to call start() 12 | # 2.3's create_window() blocks forever effectivelly calling start() itself 13 | # 14 | # 3.2's Window has a .loaded Event that you need to subscribe to notice when the window is ready for input 15 | # 2.3 has a webview_ready() function that blocks until webview is ready (or timeout is passed) 16 | import webview # Python3-webview in Debian, pywebview in pypi 17 | 18 | from ..utils import get_text 19 | from ..i18n import _ 20 | from . import helpers 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | if TYPE_CHECKING: 25 | from jellyfin_apiclient_python import client as client_type 26 | 27 | # This makes me rather uncomfortable, but there's no easy way around this other than 28 | # importing display_mirror in helpers. Lambda needed because the 2.3 version of the JS 29 | # api adds an argument even when not used. 30 | helpers.on_escape = lambda _x=None: load_idle() 31 | 32 | 33 | class DisplayMirror(object): 34 | display_window = None 35 | 36 | def __init__(self): 37 | self.open_player_menu = lambda: None 38 | self.webview = None 39 | 40 | def get_webview(self): 41 | return self.webview 42 | 43 | # noinspection PyUnresolvedReferences 44 | def run(self): 45 | # Webview needs to be run in the MainThread. 46 | 47 | # Prepare for version 2.3 before calling create_window(), which might block forever. 48 | self.display_window = webview 49 | # Since webview.create_window might take exclusive and permanent lock on the main thread, 50 | # we need to start this wait_load function before we start webview itself. 51 | if "webview_ready" in dir(webview): 52 | threading.Thread( 53 | target=lambda: (webview.webview_ready(), load_idle()) 54 | ).start() 55 | 56 | window = webview.create_window( 57 | title="Jellyfin MPV Shim Mirror", js_api=helpers, fullscreen=True 58 | ) 59 | if window is not None: 60 | # It returned a Window object instead of blocking, we're running on 3.2 (or compatible) 61 | self.display_window = window 62 | self.webview = window 63 | 64 | # 3.2's .loaded event runs every time a new DOM is loaded as well, so not suitable for this purpose 65 | # However, 3.2's load_html waits for the DOM to be ready, so we can completely skip 66 | # waiting for that ourselves. 67 | threading.Thread(target=load_idle).start() 68 | 69 | webview.start() 70 | 71 | def stop(self): 72 | if hasattr(webview, "destroy_window"): 73 | getattr(webview, "destroy_window")() 74 | else: 75 | self.webview.destroy() 76 | 77 | def display_content(self, client: "client_type", arguments): 78 | item = client.jellyfin.get_item(arguments["Arguments"]["ItemId"]) 79 | html = get_html(server_address=client.config.data["auth.server"], item=item) 80 | self.display_window.load_html(html) 81 | # print(html) 82 | # breakpoint() 83 | return 84 | 85 | 86 | mirror = DisplayMirror() 87 | 88 | 89 | # FIXME: Add some support for some sort of theming beyond Jellyfin's css, to select user defined templates 90 | def get_html(server_address: str = None, item=None): 91 | if item: 92 | jinja_vars = { 93 | "backdrop_src": helpers.getBackdropUrl(item, server_address) or "", 94 | "image_src": helpers.getPrimaryImageUrl(item, server_address) or "", 95 | "logo_src": helpers.getLogoUrl(item, server_address) or "", 96 | "played": item["UserData"].get("Played", False), 97 | "played_percentage": item["UserData"].get("PlayedPercentage", 0), 98 | "unplayed_items": item["UserData"].get("UnplayedItemCount", 0), 99 | "is_folder": item["IsFolder"], 100 | "display_name": helpers.getDisplayName(item), 101 | "misc_info_html": helpers.getMiscInfoHtml(item), 102 | "rating_html": helpers.getRatingHtml(item), 103 | "genres": item["Genres"], 104 | "overview": item.get("Overview", ""), 105 | # I believe these are all specifically for albums 106 | "poster_src": helpers.getPrimaryImageUrl(item, server_address) or "", 107 | "title": "title", # FIXME 108 | "secondary_title": "secondary", # FIXME 109 | "artist": "artist", # FIXME 110 | "album_title": "album", # FIXME 111 | } 112 | else: 113 | jinja_vars = { 114 | "random_backdrop": True, # Make the jinja template load some extra JS code for random backdrops 115 | "backdrop_src": helpers.getRandomBackdropUrl(), # Preinitialise it with a random backdrop though 116 | "display_name": _("Ready to cast"), 117 | "overview": "\n\n" 118 | + _( 119 | "Select your media in Jellyfin and play it here" 120 | ), # FIME: Mention the player_name here 121 | } 122 | 123 | jinja_vars.update({"jellyfin_css": get_text("display_mirror", "jellyfin.css")}) 124 | 125 | try: 126 | tpl = jinja2.Template(get_text("display_mirror", "index.html")) 127 | return tpl.render(jinja_vars) 128 | except Exception: 129 | pass 130 | 131 | 132 | def load_idle(): 133 | # FIXME: Add support for not actually having an idle screen and instead hide/close/something the window 134 | # Load the initial page before displaying any content, 135 | # and when refreshing to a blank page after idling. 136 | html = get_html() 137 | mirror.display_window.load_html(html) 138 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/svp_integration.py: -------------------------------------------------------------------------------- 1 | from .conf import settings 2 | from .i18n import _ 3 | 4 | import urllib.request 5 | import urllib.error 6 | import logging 7 | import sys 8 | import time 9 | 10 | log = logging.getLogger("svp_integration") 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from .menu import OSDMenu as OSDMenu_type 16 | from .player import PlayerManager as PlayerManager_type 17 | 18 | 19 | def list_request(path: str): 20 | try: 21 | response = urllib.request.urlopen(settings.svp_url + "?" + path) 22 | return response.read().decode("utf-8").replace("\r\n", "\n").split("\n") 23 | except urllib.error.URLError: 24 | log.error("Could not reach SVP API server.", exc_info=True) 25 | return None 26 | 27 | 28 | def simple_request(path: str): 29 | response_list = list_request(path) 30 | if response_list is None: 31 | return None 32 | if len(response_list) != 1 or " = " not in response_list[0]: 33 | return None 34 | return response_list[0].split(" = ")[1] 35 | 36 | 37 | def get_profiles(): 38 | profile_ids = list_request("list=profiles") 39 | profiles = {} 40 | for profile_id in profile_ids: 41 | profile_id = profile_id.replace("profiles.", "") 42 | if profile_id == "predef": 43 | continue 44 | if profile_id == "P10000001_1001_1001_1001_100000000001": 45 | profile_name = _("Automatic") 46 | else: 47 | profile_name = simple_request("profiles.{0}.title".format(profile_id)) 48 | if simple_request("profiles.{0}.on".format(profile_id)) == "false": 49 | continue 50 | profile_guid = "{" + profile_id[1:].replace("_", "-") + "}" 51 | profiles[profile_guid] = profile_name 52 | return profiles 53 | 54 | 55 | def get_name_from_guid(profile_id: str): 56 | profile_id = "P" + profile_id[1:-1].replace("-", "_") 57 | if profile_id == "P10000001_1001_1001_1001_100000000001": 58 | return _("Automatic") 59 | else: 60 | return simple_request("profiles.{0}.title".format(profile_id)) 61 | 62 | 63 | def get_last_profile(): 64 | return simple_request("rt.playback.last_profile") 65 | 66 | 67 | def is_svp_alive(): 68 | try: 69 | response = list_request("") 70 | return response is not None 71 | except Exception: 72 | log.error("Could not reach SVP API server.", exc_info=True) 73 | return False 74 | 75 | 76 | def is_svp_enabled(): 77 | return simple_request("rt.disabled") == "false" 78 | 79 | 80 | def is_svp_active(): 81 | response = simple_request("rt.playback.active") 82 | if response is None: 83 | return False 84 | return response != "" 85 | 86 | 87 | def set_active_profile(profile_id: str): 88 | # As far as I know, there is no way to directly set the profile. 89 | if not is_svp_active(): 90 | return False 91 | if profile_id == get_last_profile(): 92 | return True 93 | for i in range(len(list_request("list=profiles"))): 94 | list_request("!profile_next") 95 | if get_last_profile() == profile_id: 96 | return True 97 | return False 98 | 99 | 100 | def set_disabled(disabled: bool): 101 | return ( 102 | simple_request("rt.disabled={0}".format("true" if disabled else "false")) 103 | == "true" 104 | ) 105 | 106 | 107 | class SVPManager: 108 | def __init__(self, menu: "OSDMenu_type", player_manager: "PlayerManager_type"): 109 | self.menu = menu 110 | 111 | if settings.svp_enable: 112 | socket = settings.svp_socket 113 | if socket is None: 114 | if sys.platform.startswith("win32") or sys.platform.startswith( 115 | "cygwin" 116 | ): 117 | socket = "mpvpipe" 118 | else: 119 | socket = "/tmp/mpvsocket" 120 | 121 | # This actually *adds* another ipc server. 122 | player_manager.add_ipc(socket) 123 | 124 | if settings.svp_enable and not is_svp_alive(): 125 | log.error( 126 | "SVP is not reachable. Please make sure you have the API enabled." 127 | ) 128 | 129 | @staticmethod 130 | def is_available(): 131 | if not settings.svp_enable: 132 | return False 133 | if not is_svp_alive(): 134 | return False 135 | return True 136 | 137 | def menu_set_profile(self): 138 | profile_id = self.menu.menu_list[self.menu.menu_selection][2] 139 | if profile_id is None: 140 | set_disabled(True) 141 | else: 142 | set_active_profile(profile_id) 143 | # Need to re-render menu. SVP has a race condition so we wait a second. 144 | time.sleep(1) 145 | self.menu.menu_action("back") 146 | self.menu_action() 147 | 148 | def menu_set_enabled(self): 149 | set_disabled(False) 150 | 151 | # Need to re-render menu. SVP has a race condition so we wait a second. 152 | time.sleep(1) 153 | self.menu.menu_action("back") 154 | self.menu_action() 155 | 156 | def menu_action(self): 157 | if is_svp_active(): 158 | selected = 0 159 | active_profile = get_last_profile() 160 | profile_option_list = [(_("Disabled"), self.menu_set_profile, None)] 161 | for i, (profile_id, profile_name) in enumerate(get_profiles().items()): 162 | profile_option_list.append( 163 | (profile_name, self.menu_set_profile, profile_id) 164 | ) 165 | if profile_id == active_profile: 166 | selected = i + 1 167 | self.menu.put_menu(_("Select SVP Profile"), profile_option_list, selected) 168 | else: 169 | if is_svp_enabled(): 170 | self.menu.put_menu( 171 | _("SVP is Not Active"), 172 | [ 173 | (_("Disable"), self.menu_set_profile, None), 174 | (_("Retry"), self.menu_set_enabled), 175 | ], 176 | selected=1, 177 | ) 178 | else: 179 | self.menu.put_menu( 180 | _("SVP is Disabled"), [(_("Enable SVP"), self.menu_set_enabled)] 181 | ) 182 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Jellyfin MPV Shim 2 | 3 | Thank you for interest in contributing to this project! Contributing is the best way to have functionality 4 | quickly added to the project. 5 | 6 | ## Issues 7 | 8 | Feel free to create an issue for any problems or feature requests. Please include as much information as 9 | possible and be sure to check to make sure you aren't creating a duplicate issue. [Providing log messages](https://github.com/iwalton3/jellyfin-mpv-shim/wiki/Sending-Logs) is also important. Logs are sanitized by default, but you may want 10 | to review them. If you send other data, for instance a screenshot of MPV's information dialog, please 11 | be sure that there are no `api_key` values in the information you are sharing. 12 | 13 | ## Adding Major Features 14 | 15 | The core use-case of this application is to allow someone to cast media from their Jellyfin server to MPV. 16 | The most basic version of this is command-line only. I would like to retain this "degraded" mode of operation 17 | regardless of what features are added, despite the default (provided the required dependencies are installed) 18 | being to run with a system tray and GUI. 19 | 20 | If you would like to add additional features that disrupt or make the command-line workflow impossible, please 21 | allow the features to be disabled from the config file. For instance, the GUI and CLI are two separate modules 22 | that are swapped between depending on the situation. 23 | 24 | ## Adding Dependencies 25 | 26 | One of the major concerns with this project is allowing it to run on as many platforms as possible. Currently, 27 | the project is designed to run on Windows, Linux, and macOS. Currently the project is packaged using PIP for 28 | macOS and Linux and PyInstaller for Windows. Additionally, I want the project to have as long of a life as possible. 29 | If a dependency becomes uninstallable or prone to crashing, I would like to avoid the project being broken. 30 | 31 | If you wish to add a dependency, please gracefully handle the dependency not being installed. This is the 32 | policy I've used for most dependencies. If you cannot make a dependency optional, please contact me before 33 | starting development on a feature. If you PR a feature with required dependencies, I may refactor them into 34 | optional ones. 35 | 36 | Current Dependencies: 37 | - `python-mpv` - Provides `libmpv1` playback backend. 38 | - `python-mpv-jsonipc` - Provides `mpv` playback backend. (First-Party) 39 | - `jellyfin-apiclient-python` - Provides API client to Jellyfin. (First-Party) 40 | - `pywin32` - Allows window management on Windows. (Optional) 41 | - `pystray` - Provides systray icon. (Optional) 42 | - `tkinter` - Provides GUI for adding servers and viewing logs. (Optional) 43 | - `Jinja2` - Renders HTML for display mirroring. (Optional) 44 | - `pywebview` - Displays HTML for display mirroring. (Optional) 45 | - `pypresence` - Used for Discord Rich Presence integration. (Optional) 46 | 47 | ## Project Overview 48 | 49 | - `action_thread.py` - Thread to process events for the player from key input. 50 | - `bulk_subtitle.py` - Manages full-season bulk subtitle updates from the player menu. 51 | - `cli_mgr.py` - Command-line UI provider if GUI is not available or disabled. 52 | - `clients.py` - Manages auth tokens and Jellfyin client connections. 53 | - `conf.py` - The configuration file object. Contains configuration defaults and support code. 54 | - `conffile.py` - Generic module for getting settings folder locations on different platforms. 55 | - `constants.py` - Constant values for the application that apply to multiple modules. 56 | - `event_handler.py` - Handles remote control events from the Jellyfin websocket connection. 57 | - `gui_mgr.py` - Provides systray icon and tkinter GUI. 58 | - Note: This is a mess of `multiprocessing` processes and event threads to work around various bugs/issues. 59 | - `i18n.py` - Contains the application translation helpers. Many modules import the `_` function for user-facing strings. 60 | - `log_utils.py` - This implements logging routines, particularly managing the logger and sanitizing log messages. 61 | - `media.py` - Contains classes that manage media and playlists from Jellyfin. 62 | - `menu.py` - Implements the menu interface for changing options and playback parameters. 63 | - This works by drawing the menu as text on MPV and responding to keypress/remote control events. 64 | - `mouse.lua` - This is an MPV lua script that provides mouse events to MPV Shim for the menu. 65 | - `mpv_shim.py` - The main entry-point for the application. 66 | - Note: `run.py` is the entry-point for running in development and PyInstaller. 67 | - `player.py` - Implements player logic that controls MPV. Also owns the media playlist objects. 68 | - `rich_presence.py` - Module which implements Discord Rich Presence integration. 69 | - `svp_integration.py` - Implements SVP API and menu functionality for controlling SVP. 70 | - `syncplay.py` - Implements the SyncPlay time syncing events and algorithms. 71 | - Note that time syncing with the server is [part of the api client](https://github.com/iwalton3/jellyfin-apiclient-python/blob/master/jellyfin_apiclient_python/timesync_manager.py). 72 | - `timeline.py` - Thread to trigger playback events to the Jellyfin server. 73 | - Note: `player.py` is where the actual response is created. 74 | - `update_check.py` - Implements update checking, notifications, and the menu option to open the release page. 75 | - `utils.py` - Contains the playback profile and various utilities for other modules. 76 | - `video_profile.py` - Implements support for shader pack option profiles and related menu items. 77 | - `win_utils.py` - Implements window management workarounds for Windows. 78 | - `display_mirror` - Package that implements the full-screen display mirroring. 79 | - `integration` - This contains the appstream metadata, icons, and desktop files used in the Flatpak version. 80 | - `default_shader_pack` - This is where the `gen_pkg.sh` script installs the [default-shader-pack](https://github.com/iwalton3/default-shader-pack). 81 | 82 | ## Building the Project 83 | 84 | Please see the README for instructions. 85 | 86 | ## macOS Work 87 | 88 | If you have access to a machine running macOS and can work on this project, you may be able to greatly improve 89 | the experience for macOS users. There may be open issues for macOS that only you can work on. It would also be nice 90 | to find a better way to make this application available for macOS users, as the current procedure is a pain. 91 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/event_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from .conf import settings 5 | from .media import Media 6 | from .player import playerManager 7 | from .timeline import timelineManager 8 | 9 | log = logging.getLogger("event_handler") 10 | bindings = {} 11 | 12 | NAVIGATION_DICT = { 13 | "Back": "back", 14 | "Select": "ok", 15 | "MoveUp": "up", 16 | "MoveDown": "down", 17 | "MoveRight": "right", 18 | "MoveLeft": "left", 19 | "GoHome": "home", 20 | "GoToSettings": "home", 21 | } 22 | 23 | from typing import TYPE_CHECKING 24 | 25 | if TYPE_CHECKING: 26 | from jellyfin_apiclient_python import JellyfinClient as JellyfinClient_type 27 | 28 | 29 | def bind(event_name: str): 30 | def decorator(func): 31 | bindings[event_name] = func 32 | return func 33 | 34 | return decorator 35 | 36 | 37 | class EventHandler(object): 38 | mirror = None 39 | 40 | def handle_event( 41 | self, 42 | client: "JellyfinClient_type", 43 | event_name: str, 44 | arguments: dict, 45 | ): 46 | if event_name in bindings: 47 | log.debug("Handled Event {0}: {1}".format(event_name, arguments)) 48 | bindings[event_name](self, client, event_name, arguments) 49 | else: 50 | log.debug("Unhandled Event {0}: {1}".format(event_name, arguments)) 51 | 52 | @bind("Play") 53 | def play_media(self, client: "JellyfinClient_type", _event_name, arguments: dict): 54 | play_command = arguments.get("PlayCommand") 55 | if not playerManager.has_video(): 56 | play_command = "PlayNow" 57 | 58 | if play_command == "PlayNow": 59 | seq = arguments.get("StartIndex") 60 | if seq is None: 61 | seq = 0 62 | media = Media( 63 | client, 64 | arguments.get("ItemIds"), 65 | seq=seq, 66 | user_id=arguments.get("ControllingUserId"), 67 | aid=arguments.get("AudioStreamIndex"), 68 | sid=arguments.get("SubtitleStreamIndex"), 69 | srcid=arguments.get("MediaSourceId"), 70 | ) 71 | 72 | log.debug("EventHandler::playMedia %s" % media) 73 | offset = arguments.get("StartPositionTicks") 74 | if offset is not None: 75 | offset /= 10000000 76 | 77 | video = media.video 78 | if video: 79 | if settings.pre_media_cmd: 80 | os.system(settings.pre_media_cmd) 81 | playerManager.play(video, offset) 82 | timelineManager.send_timeline() 83 | if arguments.get("SyncPlayGroup") is not None: 84 | playerManager.syncplay.join_group(arguments["SyncPlayGroup"]) 85 | elif play_command == "PlayLast": 86 | playerManager.get_video().parent.insert_items( 87 | arguments.get("ItemIds"), append=True 88 | ) 89 | playerManager.upd_player_hide() 90 | elif play_command == "PlayNext": 91 | playerManager.get_video().parent.insert_items( 92 | arguments.get("ItemIds"), append=False 93 | ) 94 | playerManager.upd_player_hide() 95 | 96 | @bind("GeneralCommand") 97 | def general_command( 98 | self, client: "JellyfinClient_type", _event_name, arguments: dict 99 | ): 100 | command = arguments.get("Name") 101 | if command == "SetVolume": 102 | # There is currently a bug that causes this to be spammed, so we 103 | # only update it if the value actually changed. 104 | if playerManager.get_volume(True) != int(arguments["Arguments"]["Volume"]): 105 | playerManager.set_volume(int(arguments["Arguments"]["Volume"])) 106 | elif command == "SetAudioStreamIndex": 107 | playerManager.set_streams(int(arguments["Arguments"]["Index"]), None) 108 | elif command == "SetSubtitleStreamIndex": 109 | playerManager.set_streams(None, int(arguments["Arguments"]["Index"])) 110 | elif command == "DisplayContent": 111 | # If you have an idle command set, this will delay it. 112 | timelineManager.delay_idle() 113 | if self.mirror: 114 | self.mirror.display_content(client, arguments) 115 | elif command in ( 116 | "Back", 117 | "Select", 118 | "MoveUp", 119 | "MoveDown", 120 | "MoveRight", 121 | "MoveLeft", 122 | "GoHome", 123 | "GoToSettings", 124 | ): 125 | playerManager.menu_action(NAVIGATION_DICT[command]) 126 | elif command in ("Mute", "Unmute"): 127 | playerManager.set_mute(command == "Mute") 128 | elif command == "TakeScreenshot": 129 | playerManager.screenshot() 130 | elif command == "ToggleFullscreen" or command is None: 131 | # Currently when you hit the fullscreen button, no command is specified... 132 | playerManager.toggle_fullscreen() 133 | 134 | @bind("Playstate") 135 | def play_state(self, _client: "JellyfinClient_type", _event_name, arguments: dict): 136 | command = arguments.get("Command") 137 | if command == "PlayPause": 138 | playerManager.toggle_pause() 139 | elif command == "Pause": 140 | playerManager.pause_if_playing() 141 | elif command == "Unpause": 142 | playerManager.play_if_paused() 143 | elif command == "PreviousTrack": 144 | playerManager.play_prev() 145 | elif command == "NextTrack": 146 | playerManager.play_next() 147 | elif command == "Stop": 148 | playerManager.stop() 149 | elif command == "Seek": 150 | playerManager.seek( 151 | arguments.get("SeekPositionTicks") / 10000000, absolute=True 152 | ) 153 | 154 | @bind("PlayPause") 155 | def pause_play(self, _client: "JellyfinClient_type", _event_name, _arguments: dict): 156 | playerManager.toggle_pause() 157 | timelineManager.send_timeline() 158 | 159 | @bind("SyncPlayGroupUpdate") 160 | def sync_play_group_update( 161 | self, client: "JellyfinClient_type", _event_name, arguments: dict 162 | ): 163 | playerManager.syncplay.client = client 164 | playerManager.syncplay.process_group_update(arguments) 165 | 166 | @bind("SyncPlayCommand") 167 | def sync_play_command( 168 | self, client: "JellyfinClient_type", _event_name, arguments: dict 169 | ): 170 | playerManager.syncplay.client = client 171 | playerManager.syncplay.process_command(arguments) 172 | 173 | 174 | eventHandler = EventHandler() 175 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/conf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import uuid 4 | import socket 5 | import json 6 | import os.path 7 | import sys 8 | import getpass 9 | from typing import Optional 10 | from .settings_base import SettingsBase 11 | 12 | log = logging.getLogger("conf") 13 | config_path = None 14 | 15 | 16 | def get_default_sdir(): 17 | if sys.platform.startswith("win32"): 18 | if os.environ.get("USERPROFILE"): 19 | return os.path.join(os.environ["USERPROFILE"], "Desktop") 20 | else: 21 | username = getpass.getuser() 22 | return os.path.join(r"C:\Users", username, "Desktop") 23 | else: 24 | return None 25 | 26 | 27 | class Settings(SettingsBase): 28 | player_name: str = socket.gethostname() 29 | audio_output: str = "hdmi" 30 | client_uuid: str = str(uuid.uuid4()) 31 | media_ended_cmd: Optional[str] = None 32 | pre_media_cmd: Optional[str] = None 33 | stop_cmd: Optional[str] = None 34 | auto_play: bool = True 35 | idle_cmd: Optional[str] = None 36 | idle_cmd_delay: int = 60 37 | direct_paths: bool = False 38 | remote_direct_paths: bool = False 39 | always_transcode: bool = False 40 | transcode_h265: bool = False 41 | transcode_hi10p: bool = False 42 | remote_kbps: int = 10000 43 | local_kbps: int = 2147483 44 | subtitle_size: int = 100 45 | subtitle_color: str = "#FFFFFFFF" 46 | subtitle_position: str = "bottom" 47 | fullscreen: bool = True 48 | enable_gui: bool = True 49 | media_key_seek: bool = False 50 | mpv_ext: bool = sys.platform.startswith("darwin") 51 | mpv_ext_path: Optional[str] = None 52 | mpv_ext_ipc: Optional[str] = None 53 | mpv_ext_start: bool = True 54 | mpv_ext_no_ovr: bool = False 55 | enable_osc: bool = True 56 | use_web_seek: bool = False 57 | display_mirroring: bool = False 58 | log_decisions: bool = False 59 | mpv_log_level: str = "info" 60 | idle_when_paused: bool = False 61 | stop_idle: bool = False 62 | transcode_to_h265: bool = False 63 | kb_stop: str = "q" 64 | kb_prev: str = "<" 65 | kb_next: str = ">" 66 | kb_watched: str = "w" 67 | kb_unwatched: str = "u" 68 | kb_menu: str = "c" 69 | kb_menu_esc: str = "esc" 70 | kb_menu_ok: str = "enter" 71 | kb_menu_left: str = "left" 72 | kb_menu_right: str = "right" 73 | kb_menu_up: str = "up" 74 | kb_menu_down: str = "down" 75 | kb_pause: str = "space" 76 | kb_fullscreen: str = "f" 77 | kb_debug: str = "~" 78 | kb_kill_shader: str = "k" 79 | seek_up: int = 60 80 | seek_down: int = -60 81 | seek_right: int = 5 82 | seek_left: int = -5 83 | seek_v_exact: bool = False 84 | seek_h_exact: bool = False 85 | shader_pack_enable: bool = True 86 | shader_pack_custom: bool = False 87 | shader_pack_remember: bool = True 88 | shader_pack_profile: Optional[str] = None 89 | svp_enable: bool = False 90 | svp_url: str = "http://127.0.0.1:9901/" 91 | svp_socket: Optional[str] = None 92 | sanitize_output: bool = True 93 | write_logs: bool = False 94 | playback_timeout: int = 30 95 | sync_max_delay_speed: int = 50 96 | sync_max_delay_skip: int = 300 97 | sync_method_thresh: int = 2000 98 | sync_speed_time: int = 1000 99 | sync_speed_attempts: int = 3 100 | sync_attempts: int = 5 101 | sync_revert_seek: bool = True 102 | sync_osd_message: bool = True 103 | screenshot_menu: bool = True 104 | check_updates: bool = True 105 | notify_updates: bool = True 106 | lang: Optional[str] = None 107 | discord_presence: bool = False 108 | ignore_ssl_cert: bool = False 109 | menu_mouse: bool = True 110 | media_keys: bool = True 111 | connect_retry_mins: int = 0 112 | transcode_warning: bool = True 113 | lang_filter: str = "und,eng,jpn,mis,mul,zxx" 114 | lang_filter_sub: bool = False 115 | lang_filter_audio: bool = False 116 | screenshot_dir: Optional[str] = get_default_sdir() 117 | 118 | def __get_file(self, path: str, mode: str = "r", create: bool = True): 119 | created = False 120 | 121 | if not os.path.exists(path): 122 | try: 123 | _fh = open(path, mode) 124 | except IOError as e: 125 | if e.errno == 2 and create: 126 | fh = open(path, "w") 127 | json.dump(self.dict(), fh, indent=4, sort_keys=True) 128 | fh.close() 129 | created = True 130 | else: 131 | raise e 132 | except Exception: 133 | log.error("Error opening settings from path: %s" % path) 134 | return None 135 | 136 | # This should work now 137 | return open(path, mode), created 138 | 139 | def load(self, path: str, create: bool = True): 140 | global config_path # Don't want in model. 141 | fh, created = self.__get_file(path, "r", create) 142 | config_path = path 143 | if not created: 144 | try: 145 | data = json.load(fh) 146 | safe_data = self.parse_obj(data) 147 | 148 | # Copy and count items 149 | input_params = 0 150 | for key in safe_data.__fields_set__: 151 | setattr(self, key, getattr(safe_data, key)) 152 | input_params += 1 153 | 154 | # Print warnings 155 | for key, value in data.items(): 156 | if key not in safe_data.__fields_set__: 157 | log.warning("Config item {0} was ignored.".format(key)) 158 | elif value != getattr(safe_data, key): 159 | log.warning( 160 | "Config item {0} was was coerced from {1} to {2}.".format( 161 | key, repr(value), repr(getattr(safe_data, key)) 162 | ) 163 | ) 164 | 165 | log.info("Loaded settings from json: %s" % path) 166 | if input_params < len(self.__fields__): 167 | log.info("Saving back due to schema change.") 168 | self.save() 169 | except Exception as e: 170 | log.error("Error loading settings from json: %s" % e) 171 | fh.close() 172 | return False 173 | 174 | fh.close() 175 | return True 176 | 177 | def save(self): 178 | if config_path is None: 179 | raise FileNotFoundError("Config path not set.") 180 | 181 | # noinspection PyTypeChecker 182 | fh, created = self.__get_file(config_path, "w", True) 183 | 184 | try: 185 | json.dump(self.dict(), fh, indent=4, sort_keys=True) 186 | fh.flush() 187 | fh.close() 188 | except Exception as e: 189 | log.error("Error saving settings to json: %s" % e) 190 | return False 191 | 192 | return True 193 | 194 | 195 | settings = Settings() 196 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/video_profile.py: -------------------------------------------------------------------------------- 1 | from .conf import settings 2 | from . import conffile 3 | from .utils import get_resource 4 | from .constants import APP_NAME 5 | from .i18n import _ 6 | import logging 7 | import os.path 8 | import shutil 9 | import json 10 | 11 | from typing import TYPE_CHECKING 12 | 13 | if TYPE_CHECKING: 14 | from .player import PlayerManager as PlayerManager_type 15 | from .menu import OSDMenu as OSDMenu_type 16 | 17 | profile_name_translation = { 18 | "Generic (FSRCNNX)": _("Generic (FSRCNNX)"), 19 | "Generic High (FSRCNNX x16)": _("Generic High (FSRCNNX x16)"), 20 | "Anime4K x4 Faithful (For SD)": _("Anime4K x4 Faithful (For SD)"), 21 | "Anime4K x4 Perceptual (For SD)": _("Anime4K x4 Perceptual (For SD)"), 22 | "Anime4K x4 Perceptual + Deblur (For SD)": _( 23 | "Anime4K x4 Perceptual + Deblur (For SD)" 24 | ), 25 | "Anime4K x2 Faithful (For HD)": _("Anime4K x2 Faithful (For HD)"), 26 | "Anime4K x2 Perceptual (For HD)": _("Anime4K x2 Perceptual (For HD)"), 27 | "Anime4K x2 Perceptual + Deblur (For HD)": _( 28 | "Anime4K x2 Perceptual + Deblur (For HD)" 29 | ), 30 | } 31 | 32 | log = logging.getLogger("video_profile") 33 | 34 | 35 | class MPVSettingError(Exception): 36 | """Raised when MPV does not support a required setting.""" 37 | 38 | pass 39 | 40 | 41 | class VideoProfileManager: 42 | def __init__( 43 | self, menu: "OSDMenu_type", player_manager: "PlayerManager_type", player 44 | ): 45 | self.menu = menu 46 | self.playerManager = player_manager 47 | self.used_settings = set() 48 | self.current_profile = None 49 | self.player = player 50 | 51 | shader_pack_builtin = get_resource("default_shader_pack") 52 | 53 | # Load shader pack 54 | self.shader_pack = shader_pack_builtin 55 | if settings.shader_pack_custom: 56 | self.shader_pack = conffile.get(APP_NAME, "shader_pack") 57 | if not os.path.exists(self.shader_pack): 58 | shutil.copytree(shader_pack_builtin, self.shader_pack) 59 | 60 | if not os.path.exists(os.path.join(self.shader_pack, "pack.json")): 61 | raise FileNotFoundError("Could not find default shader pack.") 62 | 63 | with open(os.path.join(self.shader_pack, "pack.json")) as fh: 64 | pack = json.load(fh) 65 | self.default_groups = pack.get("default-setting-groups") or [] 66 | self.profiles = pack.get("profiles") or {} 67 | self.groups = pack.get("setting-groups") or {} 68 | self.revert_ignore = set(pack.get("setting-revert-ignore") or []) 69 | 70 | self.defaults = {} 71 | for group in self.groups.values(): 72 | setting_group = group.get("settings") 73 | if setting_group is None: 74 | continue 75 | 76 | for key, value in setting_group: 77 | if key in self.defaults or key in self.revert_ignore: 78 | continue 79 | try: 80 | self.defaults[key] = getattr(self.player, key) 81 | except Exception: 82 | log.warning( 83 | "Your MPV does not support setting {0} used in shader pack.".format( 84 | key 85 | ), 86 | exc_info=True, 87 | ) 88 | 89 | if settings.shader_pack_profile is not None: 90 | self.load_profile(settings.shader_pack_profile, reset=False) 91 | 92 | def process_setting_group( 93 | self, group_name: str, settings_to_apply: list, shaders_to_apply: list 94 | ): 95 | group = self.groups[group_name] 96 | for key, value in group.get("settings", []): 97 | if key not in self.defaults: 98 | if key not in self.revert_ignore: 99 | raise MPVSettingError( 100 | "Cannot use setting group {0} due to MPV not supporting {1}".format( 101 | group_name, key 102 | ) 103 | ) 104 | else: 105 | self.used_settings.add(key) 106 | settings_to_apply.append((key, value)) 107 | for shader in group.get("shaders", []): 108 | shaders_to_apply.append(os.path.join(self.shader_pack, "shaders", shader)) 109 | 110 | def load_profile(self, profile_name: str, reset: bool = True): 111 | if reset: 112 | self.unload_profile() 113 | log.info("Loading shader profile {0}.".format(profile_name)) 114 | profile = self.profiles[profile_name] 115 | settings_to_apply = [] 116 | shaders_to_apply = [] 117 | try: 118 | # Read Settings & Shaders 119 | for group in self.default_groups: 120 | self.process_setting_group(group, settings_to_apply, shaders_to_apply) 121 | for group in profile.get("setting-groups", []): 122 | self.process_setting_group(group, settings_to_apply, shaders_to_apply) 123 | for shader in profile.get("shaders", []): 124 | shaders_to_apply.append( 125 | os.path.join(self.shader_pack, "shaders", shader) 126 | ) 127 | 128 | # Apply Settings 129 | already_set = set() 130 | for key, value in settings_to_apply: 131 | if (key, value) in already_set: 132 | continue 133 | log.debug("Set MPV setting {0} to {1}".format(key, value)) 134 | setattr(self.player, key, value) 135 | already_set.add((key, value)) 136 | 137 | # Apply Shaders 138 | log.debug("Set shaders: {0}".format(shaders_to_apply)) 139 | self.player.glsl_shaders = shaders_to_apply 140 | self.current_profile = profile_name 141 | return True 142 | except MPVSettingError: 143 | log.error("Could not apply shader profile.", exc_info=True) 144 | return False 145 | 146 | def unload_profile(self): 147 | log.info("Unloading shader profile.") 148 | self.player.glsl_shaders = [] 149 | for setting in self.used_settings: 150 | value = self.defaults[setting] 151 | try: 152 | setattr(self.player, setting, value) 153 | except Exception: 154 | log.warning( 155 | "Default setting {0} value {1} is invalid.".format(setting, value) 156 | ) 157 | self.current_profile = None 158 | 159 | def menu_handle(self): 160 | profile_name = self.menu.menu_list[self.menu.menu_selection][2] 161 | settings_were_successful = True 162 | if profile_name is None: 163 | self.unload_profile() 164 | else: 165 | settings_were_successful = self.load_profile(profile_name) 166 | if settings.shader_pack_remember and settings_were_successful: 167 | settings.shader_pack_profile = profile_name 168 | settings.save() 169 | 170 | # Need to re-render menu. 171 | self.menu.menu_action("back") 172 | self.menu_action() 173 | 174 | def menu_action(self): 175 | selected = 0 176 | profile_option_list = [(_("None (Disabled)"), self.menu_handle, None)] 177 | for i, (profile_name, profile) in enumerate(self.profiles.items()): 178 | name = profile["displayname"] 179 | if name in profile_name_translation: 180 | name = profile_name_translation[name] 181 | profile_option_list.append((name, self.menu_handle, profile_name)) 182 | if profile_name == self.current_profile: 183 | selected = i + 1 184 | self.menu.put_menu(_("Select Shader Profile"), profile_option_list, selected) 185 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/display_mirror/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jellyfin 5 | 87 | 88 | 105 | 106 | 107 | 120 |
121 | loading 122 |
123 | 132 |
133 |
134 |
135 |
136 | 137 | 140 |
147 | {# FIXME: This should be blue, not green #} 148 |
149 | {% if played and not unplayed_items %} 150 | ✓ 151 | 154 | {% elif unplayed_items %} 155 | {{unplayed_items}} 156 | {% endif %} 157 |
158 | 159 | 162 |
163 | 164 |
165 |

{{display_name}}{{misc_info_html}}

166 |
{{rating_html}}
167 |

{{' / '.join(genres)}}

168 | 169 |

{{overview}}

170 |
171 |
172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import ipaddress 3 | import requests 4 | import urllib.parse 5 | from threading import Lock 6 | import logging 7 | import sys 8 | import os.path 9 | import platform 10 | 11 | from .conf import settings 12 | from datetime import datetime 13 | from functools import wraps 14 | from .constants import USER_APP_NAME 15 | from .i18n import _ 16 | 17 | from typing import TYPE_CHECKING, Optional 18 | 19 | if TYPE_CHECKING: 20 | from jellyfin_apiclient_python import JellyfinClient as JellyfinClient_type 21 | 22 | log = logging.getLogger("utils") 23 | 24 | seq_num = 0 25 | seq_num_lock = Lock() 26 | 27 | 28 | class Timer(object): 29 | def __init__(self): 30 | self.started = datetime.now() 31 | 32 | def restart(self): 33 | self.started = datetime.now() 34 | 35 | def elapsed_ms(self): 36 | return self.elapsed() * 1e3 37 | 38 | def elapsed(self): 39 | return (datetime.now() - self.started).total_seconds() 40 | 41 | 42 | def synchronous(tlockname: str): 43 | """ 44 | A decorator to place an instance based lock around a method. 45 | From: http://code.activestate.com/recipes/577105-synchronization-decorator-for-class-methods/ 46 | """ 47 | 48 | def _synched(func): 49 | @wraps(func) 50 | def _synchronizer(self, *args, **kwargs): 51 | tlock = self.__getattribute__(tlockname) 52 | tlock.acquire() 53 | try: 54 | return func(self, *args, **kwargs) 55 | finally: 56 | tlock.release() 57 | 58 | return _synchronizer 59 | 60 | return _synched 61 | 62 | 63 | def is_local_domain(client: "JellyfinClient_type"): 64 | # With Jellyfin, it is significantly more likely the user will be using 65 | # an address that is a hairpin NAT. We want to detect this and avoid 66 | # imposing limits in this case. 67 | url = client.config.data.get("auth.server", "") 68 | domain = urllib.parse.urlparse(url).hostname 69 | 70 | addr_info = socket.getaddrinfo(domain, 8096)[0] 71 | ip = addr_info[4][0] 72 | is_local = ipaddress.ip_address(ip).is_private 73 | 74 | if not is_local: 75 | if addr_info[0] == socket.AddressFamily.AF_INET: 76 | try: 77 | wan_ip = requests.get( 78 | "https://checkip.amazonaws.com/", timeout=(3, 10) 79 | ).text.strip("\r\n") 80 | return ip == wan_ip 81 | except Exception: 82 | log.warning( 83 | "checkip.amazonaws.com is unavailable. Assuming potential WAN ip is remote.", 84 | exc_info=True, 85 | ) 86 | return False 87 | elif addr_info[0] == socket.AddressFamily.AF_INET6: 88 | return False 89 | return True 90 | 91 | 92 | def mpv_color_to_plex(color: str): 93 | return "#" + color.lower()[3:] 94 | 95 | 96 | def plex_color_to_mpv(color: str): 97 | return "#FF" + color.upper()[1:] 98 | 99 | 100 | def get_profile( 101 | is_remote: bool = False, 102 | video_bitrate: Optional[int] = None, 103 | force_transcode: bool = False, 104 | is_tv: bool = False, 105 | ): 106 | if video_bitrate is None: 107 | if is_remote: 108 | video_bitrate = settings.remote_kbps 109 | else: 110 | video_bitrate = settings.local_kbps 111 | 112 | if settings.transcode_h265: 113 | transcode_codecs = "h264,mpeg4,mpeg2video" 114 | elif settings.transcode_to_h265: 115 | transcode_codecs = "h265,hevc,h264,mpeg4,mpeg2video" 116 | else: 117 | transcode_codecs = "h264,h265,hevc,mpeg4,mpeg2video" 118 | 119 | profile = { 120 | "Name": USER_APP_NAME, 121 | "MaxStreamingBitrate": video_bitrate * 1000, 122 | "MusicStreamingTranscodingBitrate": 1280000, 123 | "TimelineOffsetSeconds": 5, 124 | "TranscodingProfiles": [ 125 | {"Type": "Audio"}, 126 | { 127 | "Container": "ts", 128 | "Type": "Video", 129 | "Protocol": "hls", 130 | "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", 131 | "VideoCodec": transcode_codecs, 132 | "MaxAudioChannels": "6", 133 | }, 134 | {"Container": "jpeg", "Type": "Photo"}, 135 | ], 136 | "DirectPlayProfiles": [{"Type": "Video"}, {"Type": "Audio"}, {"Type": "Photo"}], 137 | "ResponseProfiles": [], 138 | "ContainerProfiles": [], 139 | "CodecProfiles": [], 140 | "SubtitleProfiles": [ 141 | {"Format": "srt", "Method": "External"}, 142 | {"Format": "srt", "Method": "Embed"}, 143 | {"Format": "ass", "Method": "External"}, 144 | {"Format": "ass", "Method": "Embed"}, 145 | {"Format": "sub", "Method": "Embed"}, 146 | {"Format": "sub", "Method": "External"}, 147 | {"Format": "ssa", "Method": "Embed"}, 148 | {"Format": "ssa", "Method": "External"}, 149 | {"Format": "smi", "Method": "Embed"}, 150 | {"Format": "smi", "Method": "External"}, 151 | # Jellyfin currently refuses to serve these subtitle types as external. 152 | {"Format": "pgssub", "Method": "Embed"}, 153 | # { 154 | # "Format": "pgssub", 155 | # "Method": "External" 156 | # }, 157 | {"Format": "dvdsub", "Method": "Embed"}, 158 | # { 159 | # "Format": "dvdsub", 160 | # "Method": "External" 161 | # }, 162 | {"Format": "pgs", "Method": "Embed"}, 163 | # { 164 | # "Format": "pgs", 165 | # "Method": "External" 166 | # } 167 | ], 168 | } 169 | 170 | if settings.transcode_hi10p: 171 | profile["CodecProfiles"].append( 172 | { 173 | "Type": "Video", 174 | "codec": "h264", 175 | "Conditions": [ 176 | { 177 | "Condition": "LessThanEqual", 178 | "Property": "VideoBitDepth", 179 | "Value": "8", 180 | } 181 | ], 182 | } 183 | ) 184 | 185 | if settings.always_transcode or force_transcode: 186 | profile["DirectPlayProfiles"] = [] 187 | 188 | if is_tv: 189 | profile["TranscodingProfiles"].insert( 190 | 0, 191 | { 192 | "Container": "ts", 193 | "Type": "Video", 194 | "AudioCodec": "mp3,aac", 195 | "VideoCodec": "h264", 196 | "Context": "Streaming", 197 | "Protocol": "hls", 198 | "MaxAudioChannels": "2", 199 | "MinSegments": "1", 200 | "BreakOnNonKeyFrames": True, 201 | }, 202 | ) 203 | 204 | return profile 205 | 206 | 207 | def get_sub_display_title(stream: dict): 208 | return "{0}{1} ({2})".format( 209 | stream.get("Language", _("Unkn")).capitalize(), 210 | _(" Forced") if stream.get("IsForced") else "", 211 | stream.get("Codec"), 212 | ) 213 | 214 | 215 | def get_seq(): 216 | global seq_num 217 | seq_num_lock.acquire() 218 | current = seq_num 219 | seq_num += 1 220 | seq_num_lock.release() 221 | return current 222 | 223 | 224 | def none_fallback(value, fallback): 225 | if value is None: 226 | return fallback 227 | return value 228 | 229 | 230 | def get_resource(*path): 231 | # Detect if bundled via pyinstaller. 232 | # From: https://stackoverflow.com/questions/404744/ 233 | if getattr(sys, "_MEIPASS", False): 234 | application_path = os.path.join(getattr(sys, "_MEIPASS"), "jellyfin_mpv_shim") 235 | else: 236 | application_path = os.path.dirname(os.path.abspath(__file__)) 237 | 238 | # ! Test code for Mac 239 | if getattr(sys, "frozen", False) and platform.system() == "Darwin": 240 | application_path = os.path.join(os.path.dirname(sys.executable), "../Resources") 241 | 242 | return os.path.join(application_path, *path) 243 | 244 | 245 | def get_text(*path): 246 | with open(get_resource(*path)) as fh: 247 | return fh.read() 248 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/clients.py: -------------------------------------------------------------------------------- 1 | from jellyfin_apiclient_python import JellyfinClient 2 | from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE 3 | from .conf import settings 4 | from . import conffile 5 | from getpass import getpass 6 | from .constants import CAPABILITIES, CLIENT_VERSION, USER_APP_NAME, USER_AGENT, APP_NAME 7 | from .i18n import _ 8 | 9 | import sys 10 | import os.path 11 | import json 12 | import uuid 13 | import time 14 | import logging 15 | import re 16 | 17 | log = logging.getLogger("clients") 18 | path_regex = re.compile("^(https?://)?([^/:]+)(:[0-9]+)?(/.*)?$") 19 | 20 | from typing import Optional 21 | 22 | 23 | def expo(max_value: Optional[int] = None): 24 | n = 0 25 | while True: 26 | a = 2 ** n 27 | if max_value is None or a < max_value: 28 | yield a 29 | n += 1 30 | else: 31 | yield max_value 32 | 33 | 34 | class ClientManager(object): 35 | def __init__(self): 36 | self.callback = lambda client, event_name, data: None 37 | self.credentials = [] 38 | self.clients = {} 39 | self.usernames = {} 40 | self.is_stopping = False 41 | 42 | def cli_connect(self): 43 | is_logged_in = self.try_connect() 44 | add_another = False 45 | 46 | if "add" in sys.argv: 47 | add_another = True 48 | 49 | while not is_logged_in or add_another: 50 | server = input(_("Server URL: ")) 51 | username = input(_("Username: ")) 52 | password = getpass(_("Password: ")) 53 | 54 | is_logged_in = self.login(server, username, password) 55 | 56 | if is_logged_in: 57 | log.info(_("Successfully added server.")) 58 | add_another = input(_("Add another server?") + " [y/N] ") 59 | add_another = add_another in ("y", "Y", "yes", "Yes") 60 | else: 61 | log.warning(_("Adding server failed.")) 62 | 63 | @staticmethod 64 | def client_factory(): 65 | client = JellyfinClient(allow_multiple_clients=True) 66 | client.config.data["app.default"] = True 67 | client.config.app( 68 | USER_APP_NAME, CLIENT_VERSION, settings.player_name, settings.client_uuid 69 | ) 70 | client.config.data["http.user_agent"] = USER_AGENT 71 | client.config.data["auth.ssl"] = not settings.ignore_ssl_cert 72 | return client 73 | 74 | def _connect_all(self): 75 | is_logged_in = False 76 | for server in self.credentials: 77 | if self.connect_client(server): 78 | is_logged_in = True 79 | return is_logged_in 80 | 81 | def try_connect(self): 82 | credentials_location = conffile.get(APP_NAME, "cred.json") 83 | if os.path.exists(credentials_location): 84 | with open(credentials_location) as cf: 85 | self.credentials = json.load(cf) 86 | 87 | if "Servers" in self.credentials: 88 | credentials_old = self.credentials 89 | self.credentials = [] 90 | for server in credentials_old["Servers"]: 91 | server["uuid"] = str(uuid.uuid4()) 92 | server["username"] = "" 93 | self.credentials.append(server) 94 | 95 | is_logged_in = self._connect_all() 96 | if settings.connect_retry_mins and not is_logged_in: 97 | log.warning( 98 | "Connection failed. Will retry for {0} minutes.".format( 99 | settings.connect_retry_mins 100 | ) 101 | ) 102 | for attempt in range(settings.connect_retry_mins * 2): 103 | time.sleep(30) 104 | is_logged_in = self._connect_all() 105 | if is_logged_in: 106 | break 107 | 108 | return is_logged_in 109 | 110 | def save_credentials(self): 111 | credentials_location = conffile.get(APP_NAME, "cred.json") 112 | with open(credentials_location, "w") as cf: 113 | json.dump(self.credentials, cf) 114 | 115 | def login( 116 | self, server: str, username: str, password: str, force_unique: bool = False 117 | ): 118 | if server.endswith("/"): 119 | server = server[:-1] 120 | 121 | protocol, host, port, path = path_regex.match(server).groups() 122 | 123 | if not protocol: 124 | log.warning("Adding http:// because it was not provided.") 125 | protocol = "http://" 126 | 127 | if protocol == "http://" and not port: 128 | log.warning("Adding port 8096 for insecure local http connection.") 129 | log.warning( 130 | "If you want to connect to standard http port 80, use :80 in the url." 131 | ) 132 | port = ":8096" 133 | 134 | server = "".join(filter(bool, (protocol, host, port, path))) 135 | 136 | client = self.client_factory() 137 | client.auth.connect_to_address(server) 138 | result = client.auth.login(server, username, password) 139 | if "AccessToken" in result: 140 | credentials = client.auth.credentials.get_credentials() 141 | server = credentials["Servers"][0] 142 | if force_unique: 143 | server["uuid"] = server["Id"] 144 | else: 145 | server["uuid"] = str(uuid.uuid4()) 146 | server["username"] = username 147 | if force_unique and server["Id"] in self.clients: 148 | return True 149 | self.connect_client(server) 150 | self.credentials.append(server) 151 | self.save_credentials() 152 | return True 153 | return False 154 | 155 | def setup_client(self, client: "JellyfinClient", server): 156 | def event(event_name, data): 157 | if event_name == "WebSocketDisconnect": 158 | timeout_gen = expo(100) 159 | if server["uuid"] in self.clients: 160 | while not self.is_stopping: 161 | timeout = next(timeout_gen) 162 | log.info( 163 | "No connection to server. Next try in {0} second(s)".format( 164 | timeout 165 | ) 166 | ) 167 | self._disconnect_client(server=server) 168 | time.sleep(timeout) 169 | if self.connect_client(server): 170 | break 171 | else: 172 | self.callback(client, event_name, data) 173 | 174 | client.callback = event 175 | client.callback_ws = event 176 | client.start(websocket=True) 177 | 178 | client.jellyfin.post_capabilities(CAPABILITIES) 179 | 180 | def remove_client(self, uuid: str): 181 | self.credentials = [ 182 | server for server in self.credentials if server["uuid"] != uuid 183 | ] 184 | self.save_credentials() 185 | self._disconnect_client(uuid=uuid) 186 | 187 | def connect_client(self, server): 188 | if self.is_stopping: 189 | return False 190 | 191 | is_logged_in = False 192 | client = self.client_factory() 193 | state = client.authenticate({"Servers": [server]}, discover=False) 194 | server["connected"] = state["State"] == CONNECTION_STATE["SignedIn"] 195 | if server["connected"]: 196 | is_logged_in = True 197 | self.clients[server["uuid"]] = client 198 | self.setup_client(client, server) 199 | if server.get("username"): 200 | self.usernames[server["uuid"]] = server["username"] 201 | 202 | return is_logged_in 203 | 204 | def _disconnect_client(self, uuid: Optional[str] = None, server=None): 205 | if uuid is None and server is not None: 206 | uuid = server["uuid"] 207 | 208 | if uuid not in self.clients: 209 | return 210 | 211 | if server is not None: 212 | server["connected"] = False 213 | 214 | client = self.clients[uuid] 215 | del self.clients[uuid] 216 | client.stop() 217 | 218 | def remove_all_clients(self): 219 | self.stop_all_clients() 220 | self.credentials = [] 221 | self.save_credentials() 222 | 223 | def stop_all_clients(self): 224 | for key, client in list(self.clients.items()): 225 | del self.clients[key] 226 | client.stop() 227 | 228 | def stop(self): 229 | self.is_stopping = True 230 | for client in self.clients.values(): 231 | client.stop() 232 | 233 | def get_username_from_client(self, client): 234 | # This is kind of convoluted. It may fail if a server 235 | # was added before we started saving usernames. 236 | for uuid, client2 in self.clients.items(): 237 | if client2 is client: 238 | if uuid in self.usernames: 239 | return self.usernames[uuid] 240 | for server in self.credentials: 241 | if server["uuid"] == uuid: 242 | return server.get("username", "Unknown") 243 | break 244 | 245 | return "Unknown" 246 | 247 | 248 | clientManager = ClientManager() 249 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/bulk_subtitle.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from .utils import get_sub_display_title 3 | from .i18n import _ 4 | 5 | # TODO: Should probably support automatic profiles for languages other than English and Japanese... 6 | 7 | import time 8 | import logging 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from .player import PlayerManager as PlayerManager_type 14 | 15 | Part = namedtuple("Part", ["id", "audio", "subtitle"]) 16 | Audio = namedtuple("Audio", ["id", "language_code", "name", "display_name"]) 17 | Subtitle = namedtuple( 18 | "Subtitle", ["id", "language_code", "name", "is_forced", "display_name"] 19 | ) 20 | 21 | log = logging.getLogger("bulk_subtitle") 22 | messages = [] 23 | keep_messages = 6 24 | 25 | 26 | def render_message(message, show_text): 27 | log.info(message) 28 | messages.append(message) 29 | text = _("Selecting Tracks...") 30 | for message in messages[-6:]: 31 | text += "\n " + message 32 | show_text(text, 2 ** 30, 1) 33 | 34 | 35 | def process_series(mode, player: "PlayerManager_type", m_raid=None, m_rsid=None): 36 | messages.clear() 37 | media = player.get_video() 38 | client = media.client 39 | show_text = player.show_text 40 | c_aid, c_sid = None, None 41 | c_pid = media.media_source.get("Id") 42 | 43 | success_ct = 0 44 | partial_ct = 0 45 | count = 0 46 | 47 | videos = client.jellyfin.get_season(media.item["SeriesId"], media.item["SeasonId"])[ 48 | "Items" 49 | ] 50 | for video in videos: 51 | name = "s{0}e{1:02}".format( 52 | video.get("ParentIndexNumber"), video.get("IndexNumber") 53 | ) 54 | video = client.jellyfin.get_item(video.get("Id")) 55 | for media_source in video["MediaSources"]: 56 | count += 1 57 | audio_list = [ 58 | Audio( 59 | s.get("Index"), 60 | s.get("Language"), 61 | s.get("Title"), 62 | s.get("DisplayTitle"), 63 | ) 64 | for s in media_source["MediaStreams"] 65 | if s.get("Type") == "Audio" 66 | ] 67 | subtitle_list = [ 68 | Subtitle( 69 | s.get("Index"), 70 | s.get("Language"), 71 | s.get("Title"), 72 | s.get("IsForced"), 73 | get_sub_display_title(s), 74 | ) 75 | for s in media_source["MediaStreams"] 76 | if s.get("Type") == "Subtitle" 77 | ] 78 | part = Part(media_source.get("Id"), audio_list, subtitle_list) 79 | 80 | aid = None 81 | sid = -1 82 | if mode == "subbed": 83 | audio, subtitle = get_subbed(part) 84 | if audio and subtitle: 85 | render_message( 86 | "{0}: {1} ({2})".format( 87 | name, subtitle.display_name, subtitle.name 88 | ), 89 | show_text, 90 | ) 91 | aid, sid = audio.id, subtitle.id 92 | success_ct += 1 93 | elif mode == "dubbed": 94 | audio, subtitle = get_dubbed(part) 95 | if audio and subtitle: 96 | render_message( 97 | "{0}: {1} ({2})".format( 98 | name, subtitle.display_name, subtitle.name 99 | ), 100 | show_text, 101 | ) 102 | aid, sid = audio.id, subtitle.id 103 | success_ct += 1 104 | elif audio: 105 | render_message(_("{0}: No Subtitles").format(name), show_text) 106 | aid = audio.id 107 | partial_ct += 1 108 | elif mode == "manual": 109 | if m_raid < len(part.audio) and m_rsid < len(part.subtitle): 110 | audio = part.audio[m_raid] 111 | aid = audio.id 112 | render_message( 113 | "{0} a: {1} ({2})".format(name, audio.display_name, audio.name), 114 | show_text, 115 | ) 116 | if m_rsid != -1: 117 | subtitle = part.subtitle[m_rsid] 118 | sid = subtitle.id 119 | render_message( 120 | "{0} s: {1} ({2})".format( 121 | name, subtitle.display_name, subtitle.name 122 | ), 123 | show_text, 124 | ) 125 | success_ct += 1 126 | 127 | if aid: 128 | if c_pid == part.id: 129 | c_aid, c_sid = aid, sid 130 | 131 | # This is a horrible hack to change the audio/subtitle settings without playing 132 | # the media. I checked the jelyfin source code and didn't find a better way. 133 | client.jellyfin.session_progress( 134 | { 135 | "ItemId": part.id, 136 | "AudioStreamIndex": aid, 137 | "SubtitleStreamIndex": sid, 138 | } 139 | ) 140 | 141 | else: 142 | render_message(_("{0}: Fail").format(name), show_text) 143 | 144 | if mode == "subbed": 145 | render_message( 146 | _("Set Subbed: {0} ok, {1} fail").format(success_ct, count - success_ct), 147 | show_text, 148 | ) 149 | elif mode == "dubbed": 150 | render_message( 151 | _("Set Dubbed: {0} ok, {1} audio only, {2} fail").format( 152 | success_ct, partial_ct, count - success_ct - partial_ct 153 | ), 154 | show_text, 155 | ) 156 | elif mode == "manual": 157 | render_message( 158 | _("Manual: {0} ok, {1} fail").format(success_ct, count - success_ct), 159 | show_text, 160 | ) 161 | time.sleep(3) 162 | if c_aid: 163 | render_message(_("Setting Current..."), show_text) 164 | player.put_task(player.set_streams, c_aid, c_sid) 165 | player.timeline_handle() 166 | 167 | 168 | def get_subbed(part): 169 | japanese_audio = None 170 | english_subtitles = None 171 | subtitle_weight = None 172 | 173 | for audio in part.audio: 174 | lower_title = audio.name.lower() if audio.name is not None else "" 175 | if audio.language_code != "jpn" and "japan" not in lower_title: 176 | continue 177 | if "commentary" in lower_title: 178 | continue 179 | 180 | if japanese_audio is None: 181 | japanese_audio = audio 182 | break 183 | 184 | for subtitle in part.subtitle: 185 | lower_title = subtitle.name.lower() if subtitle.name is not None else "" 186 | if subtitle.language_code != "eng" and "english" not in lower_title: 187 | continue 188 | if subtitle.is_forced: 189 | continue 190 | 191 | weight = dialogue_weight(lower_title) 192 | if subtitle_weight is None or weight < subtitle_weight: 193 | subtitle_weight = weight 194 | english_subtitles = subtitle 195 | 196 | if japanese_audio and english_subtitles: 197 | return japanese_audio, english_subtitles 198 | return None, None 199 | 200 | 201 | def get_dubbed(part): 202 | english_audio = None 203 | sign_subtitles = None 204 | subtitle_weight = None 205 | 206 | for audio in part.audio: 207 | lower_title = audio.name.lower() if audio.name is not None else "" 208 | if audio.language_code != "eng" and "english" not in lower_title: 209 | continue 210 | if "commentary" in lower_title: 211 | continue 212 | 213 | if english_audio is None: 214 | english_audio = audio 215 | break 216 | 217 | for subtitle in part.subtitle: 218 | lower_title = subtitle.name.lower() if subtitle.name is not None else "" 219 | if subtitle.language_code != "eng" and "english" not in lower_title: 220 | continue 221 | if subtitle.is_forced: 222 | sign_subtitles = subtitle 223 | break 224 | 225 | weight = sign_weight(lower_title) 226 | if weight == 0: 227 | continue 228 | 229 | if subtitle_weight is None or weight < subtitle_weight: 230 | subtitle_weight = weight 231 | sign_subtitles = subtitle 232 | 233 | if english_audio: 234 | return english_audio, sign_subtitles 235 | return None, None 236 | 237 | 238 | def dialogue_weight(text: str): 239 | if not text: 240 | return 900 241 | lower_text = text.lower() 242 | has_dialogue = ( 243 | "main" in lower_text or "full" in lower_text or "dialogue" in lower_text 244 | ) 245 | has_songs = "op/ed" in lower_text or "song" in lower_text or "lyric" in lower_text 246 | has_signs = "sign" in lower_text 247 | vendor = "bd" in lower_text or "retail" in lower_text 248 | weight = 900 249 | 250 | if has_dialogue and has_songs: 251 | weight -= 100 252 | if has_songs: 253 | weight += 200 254 | if has_dialogue and has_signs: 255 | weight -= 100 256 | elif has_signs: 257 | weight += 700 258 | if vendor: 259 | weight += 50 260 | return weight 261 | 262 | 263 | def sign_weight(text: str): 264 | if not text: 265 | return 0 266 | lower_text = text.lower() 267 | has_songs = "op/ed" in lower_text or "song" in lower_text or "lyric" in lower_text 268 | has_signs = "sign" in lower_text 269 | vendor = "bd" in lower_text or "retail" in lower_text 270 | weight = 900 271 | 272 | if not (has_songs or has_signs): 273 | return 0 274 | if has_songs: 275 | weight -= 200 276 | if has_signs: 277 | weight -= 300 278 | if vendor: 279 | weight += 50 280 | return weight 281 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/display_mirror/helpers.py: -------------------------------------------------------------------------------- 1 | import random 2 | import datetime 3 | import math 4 | 5 | from ..clients import clientManager 6 | 7 | # This started as a copy of some useful functions from jellyfin-chromecast's helpers.js and translated them to Python. 8 | # Only reason their not put straight into __init__.py is to keep the same logical separation that 9 | # jellyfin-chromecast has. 10 | # 11 | # I've since added some extra functions, and completely reworked some of the old ones such that it's 12 | # not directly compatible. 13 | # 14 | # Should this stuff be in jellyfin_apiclient_python instead? 15 | # Is this stuff in there already? 16 | # 17 | # FIXME: A lot of this could be done so much better with format-strings 18 | 19 | 20 | # noinspection PyPep8Naming,PyPep8Naming 21 | def getUrl(serverAddress, name): 22 | 23 | if not name: 24 | raise Exception("Url name cannot be empty") 25 | 26 | url = serverAddress 27 | url += "/" if not serverAddress.endswith("/") and not name.startswith("/") else "" 28 | url += name 29 | 30 | return url 31 | 32 | 33 | # noinspection PyPep8Naming,PyPep8Naming 34 | def getBackdropUrl(item, serverAddress): 35 | if item.get("BackdropImageTags"): 36 | return getUrl( 37 | serverAddress, 38 | "Items/" 39 | + item["Id"] 40 | + "/Images/Backdrop/0?tag=" 41 | + item["BackdropImageTags"][0], 42 | ) 43 | elif item.get("ParentBackdropItemId"): 44 | return getUrl( 45 | serverAddress, 46 | "Items/" 47 | + item["ParentBackdropItemId"] 48 | + "/Images/Backdrop/0?tag=" 49 | + item["ParentBackdropImageTags"][0], 50 | ) 51 | else: 52 | return None 53 | 54 | 55 | # noinspection PyPep8Naming,PyPep8Naming 56 | def getLogoUrl(item, serverAddress): 57 | if item.get("ImageTags", {}).get("Logo", None): 58 | return getUrl( 59 | serverAddress, 60 | "Items/" + item["Id"] + "/Images/Logo/0?tag=" + item["ImageTags"]["Logo"], 61 | ) 62 | elif item.get("ParentLogoItemId") and item.get("ParentLogoImageTag"): 63 | return getUrl( 64 | serverAddress, 65 | "Items/" 66 | + item["ParentLogoItemId"] 67 | + "/Images/Logo/0?tag=" 68 | + item["ParentLogoImageTag"], 69 | ) 70 | else: 71 | return None 72 | 73 | 74 | # noinspection PyPep8Naming,PyPep8Naming 75 | def getPrimaryImageUrl(item, serverAddress): 76 | if item.get("AlbumPrimaryImageTag"): 77 | return getUrl( 78 | serverAddress, 79 | "Items/" 80 | + item["AlbumId"] 81 | + "/Images/Primary?tag=" 82 | + item["AlbumPrimaryImageTag"], 83 | ) 84 | elif item.get("PrimaryImageTag"): 85 | return getUrl( 86 | serverAddress, 87 | "Items/" + item["Id"] + "/Images/Primary?tag=" + item["PrimaryImageTag"], 88 | ) 89 | elif item.get("ImageTags", {}).get("Primary"): 90 | return getUrl( 91 | serverAddress, 92 | "Items/" 93 | + item["Id"] 94 | + "/Images/Primary?tag=" 95 | + item["ImageTags"]["Primary"], 96 | ) 97 | else: 98 | return None 99 | 100 | 101 | # noinspection PyPep8Naming 102 | def getDisplayName(item): 103 | name = item.get("EpisodeTitle", item.get("Name")) 104 | 105 | if item["Type"] == "TvChannel": 106 | if item["Number"]: 107 | return item["Number"] + " " + name 108 | else: 109 | return name 110 | # NOTE: Must compare to None here because 0 is a legitimate option 111 | elif ( 112 | item["Type"] == "Episode" 113 | and item.get("IndexNumber") is not None 114 | and item.get("ParentIndexNumber") is not None 115 | ): 116 | number = f"S{item['ParentIndexNumber']} E{item['IndexNumber']}" 117 | if item.get("IndexNumberEnd"): 118 | number += "-" + item["IndexNumberEnd"] 119 | name = number + " - " + name 120 | 121 | return name 122 | 123 | 124 | # noinspection PyPep8Naming 125 | def getRatingHtml(item): 126 | html = "" 127 | 128 | if item.get("CommunityRating"): 129 | html += ( 130 | "
" 133 | ) 134 | html += '
' 135 | html += str(round(item["CommunityRating"], 1)) 136 | html += "
" 137 | 138 | if item.get("CriticRating") is not None: 139 | # FIXME: This doesn't seem to ever be triggering. Is that actually a problem? 140 | if item["CriticRating"] >= 60: 141 | html += '
' 142 | else: 143 | html += '
' 144 | 145 | html += '
' + str(item["CriticRating"]) + "%
" 146 | 147 | # Jellyfin-chromecast had this commented out already 148 | # Where's the metascore variable supposed to come from? 149 | # # if item.get(Metascore) and metascore !== false: { 150 | # # if item['Metascore'] >= 60: 151 | # # html += '
' + item['Metascore'] + '
' 152 | # # elif item['Metascore'] >= 40): 153 | # # html += '
' + item['Metascore'] + '
' 154 | # # else: 155 | # # html += '
' + item['Metascore'] + '
' 156 | 157 | return html 158 | 159 | 160 | def __convert_jf_str_datetime(jf_string): 161 | # datetime doesn't quite support fractions of a second the same way Jellyfin does them. 162 | # Best we can do is strip them out entirely. 163 | # FIXME: I think this loses timezone information, but are we getting any at all anyway? 164 | return datetime.datetime.strptime(jf_string.partition(".")[0], "%Y-%m-%dT%H:%M:%S") 165 | 166 | 167 | # noinspection PyPep8Naming,PyPep8Naming,PyPep8Naming 168 | def getMiscInfoHtml(item): 169 | # FIXME: Flake8 is complaining this function is too complex. 170 | # I agree, this needs to be cleaned up, a lot. 171 | # FIXME: This shouldn't return HTML, the template should take care of that. 172 | 173 | miscInfo = [] 174 | 175 | if item["Type"] == "Episode": 176 | if item.get("PremiereDate"): 177 | date = __convert_jf_str_datetime(item["PremiereDate"]) 178 | text = date.strftime("%x") 179 | miscInfo.append(text) 180 | 181 | if item.get("StartDate"): 182 | date = __convert_jf_str_datetime(item["StartDate"]) 183 | text = date.strftime("%x") 184 | miscInfo.append(text) 185 | 186 | if item["Type"] != "Recording": 187 | pass 188 | # Jellyfin-chromecast had this commented out already 189 | # # text = LiveTvHelpers.getDisplayTime(date) 190 | # # miscInfo.push(text) 191 | 192 | if item.get("ProductionYear") and item["Type"] == "Series": 193 | if item["Status"] == "Continuing": 194 | miscInfo.append(f"{item['ProductionYear']}-Present") 195 | elif item["ProductionYear"]: 196 | text = str(item["ProductionYear"]) 197 | if item.get("EndDate"): 198 | endYear = __convert_jf_str_datetime(item["EndDate"]).year 199 | if endYear != item["ProductionYear"]: 200 | text += "-" + str(endYear) 201 | miscInfo.append(text) 202 | 203 | if item["Type"] != "Series" and item["Type"] != "Episode": 204 | if item.get("ProductionYear"): 205 | miscInfo.append(str(item["ProductionYear"])) 206 | elif item.get("PremiereDate"): 207 | text = str(__convert_jf_str_datetime(item["PremiereDate"]).year) 208 | miscInfo.append(text) 209 | if item.get("RunTimeTicks") and item["Type"] != "Series": 210 | if item["Type"] == "Audio": 211 | # FIXME 212 | raise Exception("Haven't translated this to Python yet") 213 | # miscInfo.append(datetime.getDisplayRunningTime(item["RunTimeTicks"])) 214 | else: 215 | # Using math.ceil instead of round because I want the minutes rounded *up* specifically, 216 | # mostly because '1min' makes more sense than '0min' for a 1-59sec clip 217 | # FIXME: Alternatively, display '<1min' if it's 0? 218 | minutes = math.ceil(item["RunTimeTicks"] / 600000000) 219 | miscInfo.append(f"{minutes}min") 220 | 221 | if ( 222 | item.get("OfficialRating") 223 | and item["Type"] != "Season" 224 | and item["Type"] != "Episode" 225 | ): 226 | miscInfo.append(item["OfficialRating"]) 227 | 228 | if item.get("Video3DFormat"): 229 | miscInfo.append("3D") 230 | 231 | return "    ".join(miscInfo) 232 | 233 | 234 | # For some reason the webview 2.3 js api will send a positional argument of None when there are no 235 | # arguments being passed in. This really long argument name is here to catch that and hopefully not 236 | # eat other intentional arguments. 237 | # noinspection PyPep8Naming 238 | def getRandomBackdropUrl(_positional_arg_that_is_never_used=None, **params): 239 | # This function is to get 1 random item, so ignore those arguments 240 | params["SortBy"] = "Random" 241 | params["Limit"] = 1 242 | 243 | # Use sensible defaults for all other arguments. 244 | # Based on jellyfin-chromecast's behaviour. 245 | params["IncludeItemTypes"] = params.get("IncludeItemTypes", "Movie,Series") 246 | params["ImageTypes"] = params.get("ImageTypes", "Backdrop") 247 | params["Recursive"] = params.get("Recursive", True) 248 | params["MaxOfficialRating"] = params.get("MaxOfficialRating", "PG-13") 249 | 250 | # This application can have multiple client connections different servers at the same time. 251 | # So just pick a random one of those clients to query for the random item. 252 | client = random.choice(list(clientManager.clients.values())) 253 | item = client.jellyfin.user_items(params=params)["Items"][0] 254 | 255 | return getBackdropUrl(item, client.config.data["auth.server"]) 256 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/integration/com.github.iwalton3.jellyfin-mpv-shim.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.iwalton3.jellyfin-mpv-shim 5 | FSFAP 6 | GPL-3.0 7 | Jellyfin MPV Shim 8 | Cast-only client for Jellyfin Media Server 9 | Ian Walton 10 | 11 | 12 |

13 | Jellyfin MPV Shim is a client for the Jellyfin media server which plays media in the 14 | MPV media player. The application runs in the background and opens MPV only 15 | when media is cast to the player. The player supports most file formats, allowing you 16 | to prevent needless transcoding of your media files on the server. The player also has 17 | advanced features, such as bulk subtitle updates and launching commands on events. 18 |

19 |

20 | Please read the detailed instructions on GitHub for more details, including usage 21 | instructions and configuration details. 22 |

23 |
24 | 25 | 26 | Video 27 | AudioVideo 28 | TV 29 | Player 30 | ​ 31 | 32 | https://github.com/jellyfin/jellyfin-mpv-shim 33 | https://github.com/jellyfin/jellyfin-mpv-shim/blob/master/README.md 34 | https://github.com/jellyfin/jellyfin-mpv-shim/issues 35 | 36 | 37 | 38 | The web app casting to MPV Shim 39 | https://user-images.githubusercontent.com/8078788/78717835-392d2c00-78ef-11ea-9731-7fd4a1d8ebbe.png 40 | 41 | 42 | The application playing the MKV Test Suite 43 | https://jellyfin.org/images/mpv-shim/blender.png 44 | 45 | 46 | The built-in menu inside MPV 47 | https://jellyfin.org/images/mpv-shim/menu.png 48 | 49 | 50 | 51 | com.github.iwalton3.jellyfin-mpv-shim.desktop 52 | 53 | https://github.com/jellyfin/jellyfin-mpv-shim 54 | 55 | 56 | jellyfin-mpv-shim 57 | 58 | 59 | 60 | 61 | 62 | 63 |

Fix DPI issue on Windows.

64 |

Remove dependency on pydantic and handle some possible config errors.

65 |

Fix setting to allow fully disabling OSC. (#212)

66 |
67 |
68 | 69 | 70 |

This release drops the desktop webview mode.

71 |

Please use Jellyfin Media Player instead or cast to the application. You can set MPV Shim as the default target in the web player as of 10.7.0.

72 |
73 |
74 | 75 | 76 |

Changes:

77 |
    78 |
  • Upgrade to a newer MPV build.
  • 79 |
  • Add more locations to the update checker.
  • 80 |
81 |
82 |
83 | 84 | 85 |

This release fixes some important bugs. Changes:

86 |
    87 |
  • #197 Fix shuffle, play next, and play last.
  • 88 |
  • #179 Fix "failed to execute script" on Windows.
  • 89 |
90 |
91 |
92 | 93 | 94 |

This release fixes some important bugs. Changes:

95 |
    96 |
  • #191 Playing media from episode page crashes player logic and doesn't set subtitle/audio streams.
  • 97 |
  • #194 Fix erratic navigation in webclient caused by sending back bad display mirror events.
  • 98 |
  • #193 Handle trailing slashes in server URLs.
  • 99 |
100 |
101 |
102 | 103 | 104 |

This release fixes websocket forwarding and casting in the desktop client. Changes:

105 |
    106 |
  • Forward websocket events back to desktop web client.
  • 107 |
  • Re-enable casting in the desktop web client.
  • 108 |
  • Send remote control buttons to web client when MPV isn't open.
  • 109 |
  • Upgrade the Flatpak platform version.
  • 110 |
111 |
112 |
113 | 114 | 115 |

This release changes how the web client works to improve reliability. Changes:

116 |
    117 |
  • Map navigation buttons to seeking when menu inactive. (#146)
  • 118 |
  • Implement session passing and http-based webclient control.
  • 119 |
  • Added support for creating a .app file (#184)
  • 120 |
  • Fix "Unknown" in SyncPlay menu.
  • 121 |
  • Fix possible seek to end with SyncPlay group join.
  • 122 |
123 |
124 |
125 | 126 | 127 |

This release adds support for SyncPlay on 10.7.0. Changes:

128 |
    129 |
  • Speed up closing the player.
  • 130 |
  • Drop SyncPlay 10.6.x support.
  • 131 |
  • Upgrade the web client.
  • 132 |
133 |
134 |
135 | 136 | 137 |

This is a maintenance update and the last one to support SyncPlay on 1.6.x. Changes:

138 |
    139 |
  • Add keyboard shortcut (k) for killing shaders.
  • 140 |
  • Add track language filter.
  • 141 |
  • Validate config data and show errors when parsing fails.
  • 142 |
  • Stop SyncPlay hanging between episodes.
  • 143 |
  • Fix player flash between episodes after opening menu.
  • 144 |
  • Add an exact seek config option.
  • 145 |
  • Do not sync playback while showing the menu.
  • 146 |
  • Add config option for screenshot directory.
  • 147 |
  • Fix disable certificate validation option.
  • 148 |
  • Prepare for project move/rename to jellyfin/jellyfin-desktop.
  • 149 |
150 |
151 |
152 | 153 | 154 |

This update integrates many improvements, many directly from user feedback. Changes:

155 |
    156 |
  • First localized MPV Shim release! (#68)
  • 157 |
  • Add mouse support in the menu.
  • 158 |
  • Add Discord Rich Presence support. (#100)
  • 159 |
  • Warn on first bandwidth-related transcode each session.
  • 160 |
  • Allow completely disabling bandwith transcoding from menu.
  • 161 |
  • Add option to disable SSL cert checking. (Not Encouraged!)
  • 162 |
  • Add option to use default MPV config with external mpv.
  • 163 |
  • Add option to disable MPV media key integration.
  • 164 |
  • Add option to wait for network at startup.
  • 165 |
  • Remember size of desktop client only by default, not position.
  • 166 |
  • Remember window full-screen state between episodes.
  • 167 |
168 |
169 |
170 | 171 | 172 |

This is a maintenance update. Changes:

173 |
    174 |
  • Increase the timeout for logins. (#99)
  • 175 |
  • Add update check using GitHub. (#50)
  • 176 |
  • Prepare application for translation. (#68)
  • 177 |
  • Clarify the "Always Transcode" option. (#72)
  • 178 |
  • Add option to scale web client. (#90)
  • 179 |
  • Migrate from deprecated idle event to observer.
  • 180 |
  • Remove unused SyncPlay config options.
  • 181 |
182 |
183 |
184 | 185 | 186 |

This release adds SyncPlay support and better logging. Changes:

187 |
    188 |
  • Add support for SyncPlay!
  • 189 |
  • Add ability to log to disk.
  • 190 |
  • Remove API keys from logs by default.
  • 191 |
  • Possibly fix multi-server support.
  • 192 |
  • Upgrade web client.
  • 193 |
194 |
195 |
196 | 197 | 198 |

This release adds shader packs and SPV integration. Changes:

199 |
    200 |
  • Add support for glsl shader packs.
  • 201 |
  • Add support for controlling SVP.
  • 202 |
  • Don't crash with unset subtitle language. (#93)
  • 203 |
  • Make the settings button in the mobile app open settings.
  • 204 |
  • Upgrade web client. (Fixes issues with subtitles and stuck dialogs.)
  • 205 |
206 |
207 |
208 | 209 | 210 |

This is a maintenance release. Changes:

211 |
    212 |
  • Upgrade pywebview to fix resize hang. (#40)
  • 213 |
  • Upgrade web client to latest version.
  • 214 |
  • Prevent skipping videos when seeking too much. (#83)
  • 215 |
  • Improve handling of direct path SMB urls. (#84)
  • 216 |
  • Fix errors when websocket data is not a dict.
  • 217 |
  • Update resolution presets to real resolutions.
  • 218 |
219 |
220 |
221 | 222 | 223 |

Changes:

224 |
    225 |
  • Make esc key exit fullscreen when no menu is open.
  • 226 |
  • Handle seq being null on Nightly. (#63)
  • 227 |
  • Allow changing built-in keyboard shortcuts.
  • 228 |
  • Allow changing seek amount of arrow keys.
  • 229 |
230 |
231 |
232 | 233 | 234 |

235 | This version allows you to optionally use HEVC instead of H264 for transcoded videos. 236 | This allows for better quality streaming or more concurrent users over a slow connection, 237 | provided the server has the processing power or acceleration hardware to be able to handle it. 238 | You must enable this feature to avoid overloading servers that cannot handle it. 239 |

240 |
241 |
242 | 243 | 244 |

Changes:

245 |
    246 |
  • Fix playlists always playing first file instead of selected one. (#34 #53)
  • 247 |
  • Fix systray to open --config directory. (#45)
  • 248 |
  • Fix HiDPI support on Windows. (#51)
  • 249 |
  • Add system-wide install mode for Windows. (#49)
  • 250 |
251 |
252 |
253 | 254 | 255 |

This is the first build of Jellyfin MPV Shim to be release to flathub.

256 |

Changes:

257 |
    258 |
  • Add integration data to pip distribution.
  • 259 |
  • Read environment if defined for configuration.
  • 260 |
  • Use most recent jsonipc library.
  • 261 |
262 |
263 |
264 | 265 | 266 |

Changes:

267 |
    268 |
  • Fix user switching in the desktop client.
  • 269 |
  • Add --shim option to desktop launcher to launch shim version.
  • 270 |
  • Add shortcut option to desktop version to launch shim version.
  • 271 |
  • Make compatible with Python 3.6.
  • 272 |
  • Add support for input.conf.
  • 273 |
274 |
275 |
276 |
277 |
278 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/am/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: Automatically generated\n" 11 | "Language-Team: none\n" 12 | "Language: am\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/base.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=CHARSET\n" 14 | "Content-Transfer-Encoding: ENCODING\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | 553 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/be/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: Automatically generated\n" 11 | "Language-Team: none\n" 12 | "Language: be\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/bn/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: Automatically generated\n" 11 | "Language-Team: none\n" 12 | "Language: bn\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/eo/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: Automatically generated\n" 11 | "Language-Team: none\n" 12 | "Language: eo\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | -------------------------------------------------------------------------------- /jellyfin_mpv_shim/messages/fi/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-18 01:38+EDT\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: Automatically generated\n" 11 | "Language-Team: none\n" 12 | "Language: fi\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | 18 | #: jellyfin_mpv_shim/bulk_subtitle.py:29 19 | msgid "Selecting Tracks..." 20 | msgstr "" 21 | 22 | #: jellyfin_mpv_shim/bulk_subtitle.py:105 23 | msgid "{0}: No Subtitles" 24 | msgstr "" 25 | 26 | #: jellyfin_mpv_shim/bulk_subtitle.py:142 27 | msgid "{0}: Fail" 28 | msgstr "" 29 | 30 | #: jellyfin_mpv_shim/bulk_subtitle.py:146 31 | msgid "Set Subbed: {0} ok, {1} fail" 32 | msgstr "" 33 | 34 | #: jellyfin_mpv_shim/bulk_subtitle.py:151 35 | msgid "Set Dubbed: {0} ok, {1} audio only, {2} fail" 36 | msgstr "" 37 | 38 | #: jellyfin_mpv_shim/bulk_subtitle.py:158 39 | msgid "Manual: {0} ok, {1} fail" 40 | msgstr "" 41 | 42 | #: jellyfin_mpv_shim/bulk_subtitle.py:163 43 | msgid "Setting Current..." 44 | msgstr "" 45 | 46 | #: jellyfin_mpv_shim/clients.py:49 47 | msgid "Server URL: " 48 | msgstr "" 49 | 50 | #: jellyfin_mpv_shim/clients.py:50 51 | msgid "Username: " 52 | msgstr "" 53 | 54 | #: jellyfin_mpv_shim/clients.py:51 55 | msgid "Password: " 56 | msgstr "" 57 | 58 | #: jellyfin_mpv_shim/clients.py:56 59 | msgid "Successfully added server." 60 | msgstr "" 61 | 62 | #: jellyfin_mpv_shim/clients.py:57 63 | msgid "Add another server?" 64 | msgstr "" 65 | 66 | #: jellyfin_mpv_shim/clients.py:60 67 | msgid "Adding server failed." 68 | msgstr "" 69 | 70 | #: jellyfin_mpv_shim/display_mirror/__init__.py:116 71 | msgid "Ready to cast" 72 | msgstr "" 73 | 74 | #: jellyfin_mpv_shim/display_mirror/__init__.py:118 75 | msgid "Select your media in Jellyfin and play it here" 76 | msgstr "" 77 | 78 | #: jellyfin_mpv_shim/gui_mgr.py:155 79 | msgid "Application Log" 80 | msgstr "" 81 | 82 | #: jellyfin_mpv_shim/gui_mgr.py:247 jellyfin_mpv_shim/gui_mgr.py:337 83 | msgid "Add Server" 84 | msgstr "" 85 | 86 | #: jellyfin_mpv_shim/gui_mgr.py:248 87 | msgid "" 88 | "Could not add server.\n" 89 | "Please check your connection information." 90 | msgstr "" 91 | 92 | #: jellyfin_mpv_shim/gui_mgr.py:269 93 | msgid "Fail" 94 | msgstr "" 95 | 96 | #: jellyfin_mpv_shim/gui_mgr.py:269 97 | msgid "Ok" 98 | msgstr "" 99 | 100 | #: jellyfin_mpv_shim/gui_mgr.py:282 101 | msgid "Server Configuration" 102 | msgstr "" 103 | 104 | #: jellyfin_mpv_shim/gui_mgr.py:305 105 | msgid "Server:" 106 | msgstr "" 107 | 108 | #: jellyfin_mpv_shim/gui_mgr.py:310 109 | msgid "Username:" 110 | msgstr "" 111 | 112 | #: jellyfin_mpv_shim/gui_mgr.py:315 113 | msgid "Password:" 114 | msgstr "" 115 | 116 | #: jellyfin_mpv_shim/gui_mgr.py:340 117 | msgid "Remove Server" 118 | msgstr "" 119 | 120 | #: jellyfin_mpv_shim/gui_mgr.py:343 121 | msgid "Close" 122 | msgstr "" 123 | 124 | #: jellyfin_mpv_shim/gui_mgr.py:456 125 | msgid "Configure Servers" 126 | msgstr "" 127 | 128 | #: jellyfin_mpv_shim/gui_mgr.py:457 129 | msgid "Show Console" 130 | msgstr "" 131 | 132 | #: jellyfin_mpv_shim/gui_mgr.py:458 133 | msgid "Application Menu" 134 | msgstr "" 135 | 136 | #: jellyfin_mpv_shim/gui_mgr.py:459 137 | msgid "Open Config Folder" 138 | msgstr "" 139 | 140 | #: jellyfin_mpv_shim/gui_mgr.py:460 141 | msgid "Quit" 142 | msgstr "" 143 | 144 | #: jellyfin_mpv_shim/media.py:126 145 | msgid " (Transcode)" 146 | msgstr "" 147 | 148 | #: jellyfin_mpv_shim/menu.py:28 149 | msgid "White" 150 | msgstr "" 151 | 152 | #: jellyfin_mpv_shim/menu.py:29 153 | msgid "Yellow" 154 | msgstr "" 155 | 156 | #: jellyfin_mpv_shim/menu.py:30 157 | msgid "Black" 158 | msgstr "" 159 | 160 | #: jellyfin_mpv_shim/menu.py:31 161 | msgid "Cyan" 162 | msgstr "" 163 | 164 | #: jellyfin_mpv_shim/menu.py:32 165 | msgid "Blue" 166 | msgstr "" 167 | 168 | #: jellyfin_mpv_shim/menu.py:33 169 | msgid "Green" 170 | msgstr "" 171 | 172 | #: jellyfin_mpv_shim/menu.py:34 173 | msgid "Magenta" 174 | msgstr "" 175 | 176 | #: jellyfin_mpv_shim/menu.py:35 177 | msgid "Red" 178 | msgstr "" 179 | 180 | #: jellyfin_mpv_shim/menu.py:36 181 | msgid "Gray" 182 | msgstr "" 183 | 184 | #: jellyfin_mpv_shim/menu.py:40 185 | msgid "Tiny" 186 | msgstr "" 187 | 188 | #: jellyfin_mpv_shim/menu.py:41 189 | msgid "Small" 190 | msgstr "" 191 | 192 | #: jellyfin_mpv_shim/menu.py:42 193 | msgid "Normal" 194 | msgstr "" 195 | 196 | #: jellyfin_mpv_shim/menu.py:43 197 | msgid "Large" 198 | msgstr "" 199 | 200 | #: jellyfin_mpv_shim/menu.py:44 201 | msgid "Huge" 202 | msgstr "" 203 | 204 | #: jellyfin_mpv_shim/menu.py:130 205 | msgid "Main Menu" 206 | msgstr "" 207 | 208 | #: jellyfin_mpv_shim/menu.py:136 209 | msgid "Change Audio" 210 | msgstr "" 211 | 212 | #: jellyfin_mpv_shim/menu.py:137 213 | msgid "Change Subtitles" 214 | msgstr "" 215 | 216 | #: jellyfin_mpv_shim/menu.py:138 217 | msgid "Change Video Quality" 218 | msgstr "" 219 | 220 | #: jellyfin_mpv_shim/menu.py:139 jellyfin_mpv_shim/syncplay.py:627 221 | msgid "SyncPlay" 222 | msgstr "" 223 | 224 | #: jellyfin_mpv_shim/menu.py:145 225 | msgid "MPV Shim v{0} Release Info/Download" 226 | msgstr "" 227 | 228 | #: jellyfin_mpv_shim/menu.py:153 229 | msgid "Change Video Playback Profile" 230 | msgstr "" 231 | 232 | #: jellyfin_mpv_shim/menu.py:158 233 | msgid "Auto Set Audio/Subtitles (Entire Series)" 234 | msgstr "" 235 | 236 | #: jellyfin_mpv_shim/menu.py:163 237 | msgid "Quit and Mark Unwatched" 238 | msgstr "" 239 | 240 | #: jellyfin_mpv_shim/menu.py:166 241 | msgid "Screenshot" 242 | msgstr "" 243 | 244 | #: jellyfin_mpv_shim/menu.py:170 245 | msgid "Video Playback Profiles" 246 | msgstr "" 247 | 248 | #: jellyfin_mpv_shim/menu.py:173 249 | msgid "SVP Settings" 250 | msgstr "" 251 | 252 | #: jellyfin_mpv_shim/menu.py:177 jellyfin_mpv_shim/menu.py:485 253 | msgid "Video Preferences" 254 | msgstr "" 255 | 256 | #: jellyfin_mpv_shim/menu.py:178 jellyfin_mpv_shim/menu.py:519 257 | msgid "Player Preferences" 258 | msgstr "" 259 | 260 | #: jellyfin_mpv_shim/menu.py:179 261 | msgid "Close Menu" 262 | msgstr "" 263 | 264 | #: jellyfin_mpv_shim/menu.py:261 265 | msgid "Select Audio Track" 266 | msgstr "" 267 | 268 | #: jellyfin_mpv_shim/menu.py:298 269 | msgid "Select Subtitle Track" 270 | msgstr "" 271 | 272 | #: jellyfin_mpv_shim/menu.py:306 273 | msgid "None" 274 | msgstr "" 275 | 276 | #: jellyfin_mpv_shim/menu.py:345 277 | msgid "Select Transcode Quality" 278 | msgstr "" 279 | 280 | #: jellyfin_mpv_shim/menu.py:346 281 | msgid "Maximum" 282 | msgstr "" 283 | 284 | #: jellyfin_mpv_shim/menu.py:346 jellyfin_mpv_shim/menu.py:423 285 | msgid "No Transcode" 286 | msgstr "" 287 | 288 | #: jellyfin_mpv_shim/menu.py:382 289 | msgid "Select Audio/Subtitle for Series" 290 | msgstr "" 291 | 292 | #: jellyfin_mpv_shim/menu.py:384 293 | msgid "English Audio" 294 | msgstr "" 295 | 296 | #: jellyfin_mpv_shim/menu.py:386 297 | msgid "Japanese Audio w/ English Subtitles" 298 | msgstr "" 299 | 300 | #: jellyfin_mpv_shim/menu.py:391 301 | msgid "Manual by Track Index (Less Reliable)" 302 | msgstr "" 303 | 304 | #: jellyfin_mpv_shim/menu.py:421 305 | msgid "Select Default Transcode Profile" 306 | msgstr "" 307 | 308 | #: jellyfin_mpv_shim/menu.py:456 309 | msgid "Select Subtitle Color" 310 | msgstr "" 311 | 312 | #: jellyfin_mpv_shim/menu.py:465 313 | msgid "Select Subtitle Size" 314 | msgstr "" 315 | 316 | #: jellyfin_mpv_shim/menu.py:475 317 | msgid "Select Subtitle Position" 318 | msgstr "" 319 | 320 | #: jellyfin_mpv_shim/menu.py:477 321 | msgid "Bottom" 322 | msgstr "" 323 | 324 | #: jellyfin_mpv_shim/menu.py:478 325 | msgid "Top" 326 | msgstr "" 327 | 328 | #: jellyfin_mpv_shim/menu.py:479 329 | msgid "Middle" 330 | msgstr "" 331 | 332 | #: jellyfin_mpv_shim/menu.py:488 333 | msgid "Remote Transcode Quality: {0:0.1f} Mbps" 334 | msgstr "" 335 | 336 | #: jellyfin_mpv_shim/menu.py:494 337 | msgid "Subtitle Size: {0}" 338 | msgstr "" 339 | 340 | #: jellyfin_mpv_shim/menu.py:498 341 | msgid "Subtitle Position: {0}" 342 | msgstr "" 343 | 344 | #: jellyfin_mpv_shim/menu.py:502 345 | msgid "Subtitle Color: {0}" 346 | msgstr "" 347 | 348 | #: jellyfin_mpv_shim/menu.py:507 349 | msgid "Transcode H265 to H264" 350 | msgstr "" 351 | 352 | #: jellyfin_mpv_shim/menu.py:509 353 | msgid "Transcode Hi10p to 8bit" 354 | msgstr "" 355 | 356 | #: jellyfin_mpv_shim/menu.py:511 357 | msgid "Direct Paths" 358 | msgstr "" 359 | 360 | #: jellyfin_mpv_shim/menu.py:512 361 | msgid "Transcode to H265" 362 | msgstr "" 363 | 364 | #: jellyfin_mpv_shim/menu.py:513 365 | msgid "Disable Direct Play" 366 | msgstr "" 367 | 368 | #: jellyfin_mpv_shim/menu.py:521 369 | msgid "Auto Play" 370 | msgstr "" 371 | 372 | #: jellyfin_mpv_shim/menu.py:522 373 | msgid "Auto Fullscreen" 374 | msgstr "" 375 | 376 | #: jellyfin_mpv_shim/menu.py:523 377 | msgid "Media Key Seek" 378 | msgstr "" 379 | 380 | #: jellyfin_mpv_shim/menu.py:524 381 | msgid "Use Web Seek Pref" 382 | msgstr "" 383 | 384 | #: jellyfin_mpv_shim/menu.py:525 385 | msgid "Display Mirroring" 386 | msgstr "" 387 | 388 | #: jellyfin_mpv_shim/menu.py:526 389 | msgid "Write Logs to File" 390 | msgstr "" 391 | 392 | #: jellyfin_mpv_shim/menu.py:527 393 | msgid "Check for Updates" 394 | msgstr "" 395 | 396 | #: jellyfin_mpv_shim/menu.py:529 397 | msgid "Discord Rich Presence" 398 | msgstr "" 399 | 400 | #: jellyfin_mpv_shim/player.py:498 401 | msgid "" 402 | "Your remote video is transcoding!\n" 403 | "Press c to adjust bandwidth settings if this is not needed." 404 | msgstr "" 405 | 406 | #: jellyfin_mpv_shim/player.py:849 407 | msgid "Season {0} - Episode {1}" 408 | msgstr "" 409 | 410 | #: jellyfin_mpv_shim/svp_integration.py:45 411 | #: jellyfin_mpv_shim/svp_integration.py:58 412 | msgid "Automatic" 413 | msgstr "" 414 | 415 | #: jellyfin_mpv_shim/svp_integration.py:160 416 | msgid "Disabled" 417 | msgstr "" 418 | 419 | #: jellyfin_mpv_shim/svp_integration.py:167 420 | msgid "Select SVP Profile" 421 | msgstr "" 422 | 423 | #: jellyfin_mpv_shim/svp_integration.py:171 424 | msgid "SVP is Not Active" 425 | msgstr "" 426 | 427 | #: jellyfin_mpv_shim/svp_integration.py:173 428 | msgid "Disable" 429 | msgstr "" 430 | 431 | #: jellyfin_mpv_shim/svp_integration.py:174 432 | msgid "Retry" 433 | msgstr "" 434 | 435 | #: jellyfin_mpv_shim/svp_integration.py:180 436 | msgid "Enable SVP" 437 | msgstr "" 438 | 439 | #: jellyfin_mpv_shim/svp_integration.py:180 440 | msgid "SVP is Disabled" 441 | msgstr "" 442 | 443 | #: jellyfin_mpv_shim/syncplay.py:21 444 | msgid "The specified SyncPlay group does not exist." 445 | msgstr "" 446 | 447 | #: jellyfin_mpv_shim/syncplay.py:22 448 | msgid "Creating SyncPlay groups is not allowed." 449 | msgstr "" 450 | 451 | #: jellyfin_mpv_shim/syncplay.py:23 452 | msgid "SyncPlay group access was denied." 453 | msgstr "" 454 | 455 | #: jellyfin_mpv_shim/syncplay.py:24 456 | msgid "Access to the SyncPlay library was denied." 457 | msgstr "" 458 | 459 | #: jellyfin_mpv_shim/syncplay.py:150 460 | msgid "SpeedToSync (x{0})" 461 | msgstr "" 462 | 463 | #: jellyfin_mpv_shim/syncplay.py:161 464 | msgid "Sync Disabled (Too Many Attempts)" 465 | msgstr "" 466 | 467 | #: jellyfin_mpv_shim/syncplay.py:169 468 | msgid "SkipToSync (x{0})" 469 | msgstr "" 470 | 471 | #: jellyfin_mpv_shim/syncplay.py:230 472 | msgid "SyncPlay enabled." 473 | msgstr "" 474 | 475 | #: jellyfin_mpv_shim/syncplay.py:250 476 | msgid "SyncPlay disabled." 477 | msgstr "" 478 | 479 | #: jellyfin_mpv_shim/syncplay.py:313 480 | msgid "{0} has joined." 481 | msgstr "" 482 | 483 | #: jellyfin_mpv_shim/syncplay.py:315 484 | msgid "{0} has left." 485 | msgstr "" 486 | 487 | #: jellyfin_mpv_shim/syncplay.py:317 488 | msgid "{0} is buffering." 489 | msgstr "" 490 | 491 | #: jellyfin_mpv_shim/syncplay.py:608 492 | msgid "{0}'s Group" 493 | msgstr "" 494 | 495 | #: jellyfin_mpv_shim/syncplay.py:617 jellyfin_mpv_shim/video_profile.py:176 496 | msgid "None (Disabled)" 497 | msgstr "" 498 | 499 | #: jellyfin_mpv_shim/syncplay.py:621 500 | msgid "New Group" 501 | msgstr "" 502 | 503 | #: jellyfin_mpv_shim/update_check.py:67 504 | msgid "" 505 | "MPV Shim v{0} Update Available\n" 506 | "Open menu (press c) for details." 507 | msgstr "" 508 | 509 | #: jellyfin_mpv_shim/utils.py:208 510 | msgid "Unkn" 511 | msgstr "" 512 | 513 | #: jellyfin_mpv_shim/utils.py:209 514 | msgid " Forced" 515 | msgstr "" 516 | 517 | #: jellyfin_mpv_shim/video_profile.py:18 518 | msgid "Generic (FSRCNNX)" 519 | msgstr "" 520 | 521 | #: jellyfin_mpv_shim/video_profile.py:19 522 | msgid "Generic High (FSRCNNX x16)" 523 | msgstr "" 524 | 525 | #: jellyfin_mpv_shim/video_profile.py:20 526 | msgid "Anime4K x4 Faithful (For SD)" 527 | msgstr "" 528 | 529 | #: jellyfin_mpv_shim/video_profile.py:21 530 | msgid "Anime4K x4 Perceptual (For SD)" 531 | msgstr "" 532 | 533 | #: jellyfin_mpv_shim/video_profile.py:22 534 | msgid "Anime4K x4 Perceptual + Deblur (For SD)" 535 | msgstr "" 536 | 537 | #: jellyfin_mpv_shim/video_profile.py:25 538 | msgid "Anime4K x2 Faithful (For HD)" 539 | msgstr "" 540 | 541 | #: jellyfin_mpv_shim/video_profile.py:26 542 | msgid "Anime4K x2 Perceptual (For HD)" 543 | msgstr "" 544 | 545 | #: jellyfin_mpv_shim/video_profile.py:27 546 | msgid "Anime4K x2 Perceptual + Deblur (For HD)" 547 | msgstr "" 548 | 549 | #: jellyfin_mpv_shim/video_profile.py:184 550 | msgid "Select Shader Profile" 551 | msgstr "" 552 | --------------------------------------------------------------------------------