Skip to content

Commit

Permalink
Add systray and GUI ported from jellyfin-mpv-shim.
Browse files Browse the repository at this point in the history
  • Loading branch information
iwalton3 committed Jan 17, 2020
1 parent 4ab7532 commit 341e4d4
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 7 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include jellyfin_mpv_shim/systray.png
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ All of these settings apply to direct play and are adjustable through the contro
- If you change this, it should be changed to a profile that supports `hls` streaming.
- `sanitize_output` - Prevent Plex tokens from being printed to the console. Default: `true`
- `fullscreen` - Fullscreen the player when starting playback. Default: `true`
- `enable_gui` - Enable the system tray icon and GUI features. Default: `true`

### MPV Configuration

Expand All @@ -137,7 +138,8 @@ The project is written entierly in Python 3. There are no closed-source
components in this project. It is fully hackable.

The project is dependent on `python-mpv` and `requests`. If you are using Windows
and would like mpv to be maximize properly, `pywin32` is also needed.
and would like mpv to be maximize properly, `pywin32` is also needed. The GUI component
uses `pystray` and `tkinter`, but there is a fallback cli mode.

If you are using a local firewall, you'll want to allow inbound connections on
TCP 3000 and UDP 32410, 32412, 32413, and 32414. The TCP port is for the web
Expand All @@ -155,6 +157,11 @@ If you are on Linux, you can install via pip. You'll need [libmpv1](https://gith
```bash
sudo pip3 install --upgrade plex-mpv-shim
```
If you would like the GUI and systray features, also install:
```bash
sudo pip3 install pystray
sudo apt install python3-tk
```

The current Debian package for `libmpv1` doesn't support the on-screen controller. If you'd like this, or need codecs that aren't packaged with Debian, you need to build mpv from source. Execute the following:
```bash
Expand All @@ -175,8 +182,8 @@ following these directions, please take care to ensure both the python
and libmpv libraries are either 64 or 32 bit. (Don't mismatch them.)

1. Install [Python3](https://www.python.org/downloads/) with PATH enabled. Install [7zip](https://ninite.com/7zip/).
2. After installing python3, open `cmd` as admin and run `pip install --upgrade pyinstaller python-mpv requests pywin32`.
2. After installing python3, open `cmd` as admin and run `pip install --upgrade pyinstaller python-mpv requests pywin32 pystray`.
3. Download [libmpv](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/).
4. Extract the `mpv-1.dll` from the file and move it to the `plex-mpv-shim` folder.
5. Open a regular `cmd` prompt. Navigate to the `plex-mpv-shim` folder.
6. Run `pyinstaller -cF --add-binary "mpv-1.dll;." --icon media.ico run.py`.
6. Run `pyinstaller -wF --add-binary "mpv-1.dll;." --add-binary "jellyfin_mpv_shim\systray.png;." --icon media.ico run.py`.
12 changes: 12 additions & 0 deletions plex_mpv_shim/cli_mgr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import time

class UserInterface(object):
def __init__(self):
self.open_player_menu = lambda: None
self.stop = lambda: None

def run(self):
while True:
time.sleep(1)

userInterface = UserInterface()
1 change: 1 addition & 0 deletions plex_mpv_shim/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Settings(object):
"subtitle_color": "#FFFFFFFF",
"subtitle_position": "bottom",
"fullscreen": True,
"enable_gui": True,
}

def __getattr__(self, name):
Expand Down
181 changes: 181 additions & 0 deletions plex_mpv_shim/gui_mgr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from pystray import Icon, MenuItem, Menu
from PIL import Image
from collections import deque
import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
from multiprocessing import Process, Queue
import threading
import sys
import logging
import queue
import os.path

APP_NAME = "plex-mpv-shim"
from .conffile import confdir

if (sys.platform.startswith("win32") or sys.platform.startswith("cygwin")) and getattr(sys, 'frozen', False):
# Detect if bundled via pyinstaller.
# From: https://stackoverflow.com/questions/404744/
icon_file = os.path.join(sys._MEIPASS, "systray.png")
else:
icon_file = os.path.join(os.path.dirname(__file__), "systray.png")
log = logging.getLogger('gui_mgr')

# From https://stackoverflow.com/questions/6631299/
# This is for opening the config directory.
def _show_file_darwin(path):
subprocess.check_call(["open", path])

def _show_file_linux(path):
subprocess.check_call(["xdg-open", path])

def _show_file_win32(path):
subprocess.check_call(["explorer", "/select", path])

_show_file_func = {'darwin': _show_file_darwin,
'linux': _show_file_linux,
'win32': _show_file_win32,
'cygwin': _show_file_win32}

try:
show_file = _show_file_func[sys.platform]
def open_config():
show_file(confdir(APP_NAME))
except KeyError:
open_config = None
log.warning("Platform does not support opening folders.")

# Setup a log handler for log items.
log_cache = deque([], 1000)
root_logger = logging.getLogger('')

class GUILogHandler(logging.Handler):
def __init__(self):
self.callback = None
super().__init__()

def emit(self, record):
log_entry = self.format(record)
log_cache.append(log_entry)

if self.callback:
try:
self.callback(log_entry)
except Exception:
pass

guiHandler = GUILogHandler()
guiHandler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)8s] %(message)s"))
root_logger.addHandler(guiHandler)

# Why am I using another process for the GUI windows?
# Because both pystray and tkinter must run
# in the main thread of their respective process.

class LoggerWindow(threading.Thread):
def __init__(self):
self.dead = False
threading.Thread.__init__(self)

def run(self):
self.queue = Queue()
self.r_queue = Queue()
self.process = LoggerWindowProcess(self.queue, self.r_queue)

def handle(message):
self.handle("append", message)

self.process.start()
handle("\n".join(log_cache))
guiHandler.callback = handle
while True:
action, param = self.r_queue.get()
if action == "die":
self._die()
break

def handle(self, action, params=None):
self.queue.put((action, params))

def stop(self, is_source=False):
self.r_queue.put(("die", None))

def _die(self):
guiHandler.callback = None
self.handle("die")
self.process.terminate()
self.dead = True

class LoggerWindowProcess(Process):
def __init__(self, queue, r_queue):
self.queue = queue
self.r_queue = r_queue
Process.__init__(self)

def update(self):
try:
self.text.config(state=tk.NORMAL)
while True:
action, param = self.queue.get_nowait()
if action == "append":
self.text.config(state=tk.NORMAL)
self.text.insert(tk.END, "\n")
self.text.insert(tk.END, param)
self.text.config(state=tk.DISABLED)
self.text.see(tk.END)
elif action == "die":
self.root.destroy()
self.root.quit()
return
except queue.Empty:
pass
self.text.after(100, self.update)

def run(self):
root = tk.Tk()
self.root = root
root.title("Application Log")
text = tk.Text(root)
text.pack(side=tk.LEFT, fill=tk.BOTH, expand = tk.YES)
text.config(wrap=tk.WORD)
self.text = text
yscroll = tk.Scrollbar(command=text.yview)
text['yscrollcommand'] = yscroll.set
yscroll.pack(side=tk.RIGHT, fill=tk.Y)
text.config(state=tk.DISABLED)
self.update()
root.mainloop()
self.r_queue.put(("die", None))

class UserInterface:
def __init__(self):
self.open_player_menu = lambda: None
self.icon_stop = lambda: None
self.log_window = None

def stop(self):
if self.log_window and not self.log_window.dead:
self.log_window.stop()
self.icon_stop()

def show_console(self):
if self.log_window is None or self.log_window.dead:
self.log_window = LoggerWindow()
self.log_window.start()

def run(self):
menu_items = [
MenuItem("Show Console", self.show_console),
MenuItem("Application Menu", self.open_player_menu),
]

if open_config:
menu_items.append(MenuItem("Open Config Folder", open_config))
menu_items.append(MenuItem("Quit", self.stop))
icon = Icon(APP_NAME, menu=Menu(*menu_items))
icon.icon = Image.open(icon_file)
self.icon_stop = icon.stop
icon.run()

userInterface = UserInterface()
15 changes: 13 additions & 2 deletions plex_mpv_shim/mpv_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ def main():
settings.load(conf_file)
settings.add_listener(update_gdm_settings)

use_gui = False
if settings.enable_gui:
try:
from .gui_mgr import userInterface
use_gui = True
except Exception:
log.warning("Cannot load GUI. Falling back to command line interface.", exc_info=1)

if not use_gui:
from .cli_mgr import userInterface

update_gdm_settings()
gdm.start_all()

Expand All @@ -45,10 +56,10 @@ def main():
playerManager.timeline_trigger = timelineManager.trigger
actionThread.start()
playerManager.action_trigger = actionThread.trigger
userInterface.open_player_menu = playerManager.menu.show_menu

try:
while True:
time.sleep(1)
userInterface.run()
except KeyboardInterrupt:
print("")
log.info("Stopping services...")
Expand Down
Binary file added plex_mpv_shim/systray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Newer revisions of python-mpv require mpv-1.dll in the PATH.
import os
import sys
import multiprocessing
if sys.platform.startswith("win32") or sys.platform.startswith("cygwin"):
# Detect if bundled via pyinstaller.
# From: https://stackoverflow.com/questions/404744/
Expand All @@ -13,4 +14,8 @@
os.environ["PATH"] = application_path + os.pathsep + os.environ["PATH"]

from plex_mpv_shim.mpv_shim import main
main()
if __name__ == '__main__':
# https://stackoverflow.com/questions/24944558/pyinstaller-built-windows-exe-fails-with-multiprocessing
multiprocessing.freeze_support()

main()
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@
"Operating System :: OS Independent",
],
python_requires='>=3.6',
install_requires=['python-mpv', 'requests']
install_requires=['python-mpv', 'requests'],
include_package_data=True

)

0 comments on commit 341e4d4

Please sign in to comment.