├── 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 |
4 |
5 |
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 |
16 |
17 |
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 |
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 |
--------------------------------------------------------------------------------