#!/usr/bin/env python3
# Thanks to https://github.com/pogmommy for making the original macOS installer
# Thanks to https://github.com/xenoncolt for making the original Windows installer
# Their contributions made this universal script a lot easier to produce.
import json
import os
import subprocess
import platform
from time import sleep
import sys
# customizable confirm prompt
def confirm(message: str = "Continue", default: bool | None = None, direct: bool | None = None) -> bool:
prompts = {True: "(Y/n)", False: "(y/N)", None: "(y/n)"}
full_message = f"{message} {prompts[default]}: "
valid_inputs = {"y": True, "yes": True, "n": False, "no": False}
if default is not None:
valid_inputs[""] = default
while (response := input(full_message).strip().lower()) not in valid_inputs:
print("Invalid input, please type y or n")
output = valid_inputs[response]
if direct is not None and not output:
return None
return output
path = ""
if platform.system() != "Windows":
if os.environ.get("XDG_CONFIG_HOME"):
path = os.environ["XDG_CONFIG_HOME"].removesuffix("/") + "/jellyfin-rpc/"
else:
path = os.environ["HOME"].removesuffix("/") + "/.config/jellyfin-rpc/"
subprocess.run(["mkdir", "-p", path])
else:
path = os.environ["APPDATA"].removesuffix("\\") + "\\jellyfin-rpc\\"
subprocess.run(
["powershell", "-Command", f'mkdir "{path}"'],
stdout=subprocess.DEVNULL,
)
print("""
Welcome to the Jellyfin-RPC installer
[https://github.com/Radiicall/jellyfin-rpc#Setup]
""")
config_path = path + "main.json"
use_existing = False
if os.path.isfile(config_path):
print(f"Found existing config: {config_path}")
if "--use-existing-config" in sys.argv:
print("Using existing config")
use_existing = True
else:
use_existing = confirm(message="Use existing config?", default=False)
if not use_existing:
print("----------Jellyfin----------")
url = input("URL (include http/https): ")
api_key = input(f"API key [Create one here: {url}/web/index.html#!/apikeys.html]: ")
print(
"Enter a single username or enter multiple usernames in a comma separated list."
)
username = input("username[s]: ").split(",")
self_signed_cert = None
if url.startswith("https://"):
self_signed_cert = confirm(
message="Are you using a self signed certificate?", default=False, direct=True
)
print(
"If you dont want anything else you can just press enter through all of these"
)
music = confirm(message="Do you want to customize music display?", default=False)
if music:
print("Enter what you would like to be shown in a comma seperated list")
print("Remember that it will show in the order you type it in")
print("Valid options are year, album and/or genres")
display = input("[Default: genres]: ").split(",")
print("Choose the separator between the artist name and the info")
separator = input("[Default: -]: ")
if display == "":
display = None
if separator == "":
separator = None
music = {"display": display, "separator": separator}
else:
music = None
movies = confirm(message="Do you want to customize movie display?", default=False)
if movies:
print("Enter what you would like to be shown in a comma seperated list")
print("Remember that it will show in the order you type it in")
print("Valid options are year, critic-score, community-score and/or genres")
display = input("[Default: genres]: ").split(",")
print("Choose the separator between the artist name and the info")
separator = input("[Default: -]: ")
if display == "":
display = None
if separator == "":
separator = None
movies = {"display": display, "separator": separator}
else:
movies = None
blacklist = confirm(
message="Do you want to blacklist media types or libraries?", default=False
)
if blacklist:
print(
"You will first type in what media types to blacklist, this should be a comma separated list WITHOUT SPACES"
)
print(
"then after that you can choose what libraries to blacklist, this should ALSO be a comma separated list,"
)
print(
"there should be no spaces before or after the commas but there can be spaces in the names of libraries"
)
sleep(2)
print("Media types 1/2")
media_types = input(
"Valid types are music, movie, episode and/or livetv [Default: ]: "
).split(",")
print("Libraries 2/2")
libraries = input("Enter libraries to blacklist [Default: ]: ").split(",")
blacklist = {"media_types": media_types, "libraries": libraries}
else:
blacklist = None
show_simple = confirm(
message="Do you want to show episode names in RPC?", default=True, direct=True
)
append_prefix = confirm(
"Do you want to add a leading 0 to season and episode numbers?", default=False, direct=True
)
add_divider = confirm(
"Do you want to add a divider between numbers, ex. S01 - E01?", default=False, direct=True
)
jellyfin = {
"url": url,
"api_key": api_key,
"username": username,
"music": music,
"movies": movies,
"blacklist": blacklist,
"self_signed_cert": self_signed_cert,
"show_simple": show_simple,
"append_prefix": append_prefix,
"add_divider": add_divider,
}
print("----------Discord----------")
appid = input("Enter your discord application ID [Default: 1053747938519679018]: ")
if appid == "":
appid = None
show_paused = confirm(message="Do you want to show paused videos?", default=True, direct=True)
print("----------Buttons----------")
buttons = confirm(message="Do you want custom buttons?", default=False)
if buttons:
buttons = []
print(
"If you want one button to continue being dynamic then you have to specifically enter dynamic into both name and url fields"
)
print(
"If you dont want any buttons to appear then you can leave everything blank here and it wont show anything."
)
print("Button 1/2")
name = input("Choose what the button will show [Default: dynamic]: ")
url = input("Choose where the button will direct to [Default: dynamic]: ")
if name != "" and url != "":
buttons.append({"name": name, "url": url})
print("Button 2/2")
name = input("Choose what the button will show [Default: dynamic]: ")
url = input("Choose where the button will direct to [Default: dynamic]: ")
if name != "" and url != "":
buttons.append({"name": name, "url": url})
else:
buttons = None
print("----------Images----------")
images = confirm(message="Do you want images?", default=False)
if images:
imgur_images = confirm("Do you want imgur images?", default=False, direct=True)
if imgur_images:
client_id = input("Enter your imgur client id: ")
imgur = {"client_id": client_id}
else:
imgur = None
images = {
"enable_images": True,
"imgur_images": imgur_images,
}
else:
imgur = None
images = None
discord = {"application_id": appid, "buttons": buttons, "show_paused": show_paused}
config = {
"jellyfin": jellyfin,
"discord": discord,
"imgur": imgur,
"images": images,
}
print(f"\nPlacing config in '{path}'")
file = open(config_path, "w")
file.write(json.dumps(config, indent=2))
file.close()
if "--no-install" in sys.argv:
print("Skipping installation")
exit(0)
continue_setup = confirm(message="Do you want to download Jellyfin-RPC?", default=True)
if not continue_setup:
print("Exiting...")
exit(0)
print("\nDownloading Jellyfin-RPC")
if platform.system() == "Windows":
subprocess.run(
[
"curl",
"-o",
path + "jellyfin-rpc.exe",
"-L",
"https://github.com/Radiicall/jellyfin-rpc/releases/latest/download/jellyfin-rpc.exe",
]
)
autostart = confirm(
message="Do you want to autostart Jellyfin-RPC at login?", default=False
)
if autostart:
if os.path.isfile(path + "winsw.exe"):
print(
"The script will prompt for administrator to remove the already installed service"
)
sleep(1)
subprocess.run([path + "winsw.exe", "uninstall"])
subprocess.run(
[
"curl",
"-o",
path + "winsw.exe",
"-L",
"https://github.com/winsw/winsw/releases/latest/download/WinSW-x64.exe",
]
)
content = f"""
jellyfin-rpc
Jellyfin-RPC
This service is running Jellyfin-RPC for rich presence support
{path}jellyfin-rpc.exe
-c {path}main.json -i {path}urls.json
"""
file = open(path + "winsw.xml", "w")
file.write(content)
file.close()
print(
"The program will now ask you for administrator rights twice, this is so the service can be installed!"
)
print("waiting 5 seconds")
sleep(5)
subprocess.run([path + "winsw.exe", "install"])
subprocess.run([path + "winsw.exe", "start"])
print(
"Autostart has been set up, jellyfin-rpc should now launch at login\nas long as there are no issues with the configuration"
)
elif platform.system() == "Darwin":
file = f"https://github.com/Radiicall/jellyfin-rpc/releases/latest/download/jellyfin-rpc-{platform.machine()}-darwin"
subprocess.run(["curl", "-o", "/usr/local/bin/jellyfin-rpc", "-L", file])
subprocess.run(["chmod", "+x", "/usr/local/bin/jellyfin-rpc"])
autostart = confirm(
message="Do you want to autostart Jellyfin-RPC at login?", default=False
)
if autostart:
if subprocess.run(["pgrep", "-xq", "--", "'jellyfin-rpc'"]).returncode == 0:
subprocess.run(["killall", "jellyfin-rpc"])
if (
"Jellyfin-RPC"
in subprocess.Popen("launchctl list", shell=True, stdout=subprocess.PIPE)
.stdout.read()
.decode()
):
subprocess.run(["launchctl", "remove", "Jellyfin-RPC"])
content = """
Label
Jellyfin-RPC
Program
/usr/local/bin/jellyfin-rpc
RunAtLoad
StandardErrorPath
/tmp/jellyfinrpc.local.stderr.txt
StandardOutPath
/tmp/jellyfinrpc.local.stdout.txt
"""
path = os.environ["HOME"] + "/Library/LaunchAgents/jellyfinrpc.local.plist"
file = open(path, "w")
file.write(content)
file.close()
subprocess.run(["chmod", "644", path])
subprocess.run(["launchctl", "load", path])
print("Jellyfin RPC is now set up to start at login.")
print(
"If needed, you can run Jellyfin RPC at any time by running 'jellyfin-rpc' in a terminal."
)
else:
# If ARM64
if "aarch64" in platform.machine().lower() or "armv8" in platform.machine().lower():
linux_binary = "jellyfin-rpc-arm64-linux"
# Else If ARM32
elif "aarch" in platform.machine().lower() or "arm" in platform.machine().lower():
linux_binary = "jellyfin-rpc-arm32-linux"
else:
linux_binary = "jellyfin-rpc-x86_64-linux"
subprocess.run(
["mkdir", "-p", os.environ["HOME"].removesuffix("/") + "/.local/bin"]
)
subprocess.run(
[
"curl",
"-o",
os.environ["HOME"].removesuffix("/") + "/.local/bin/jellyfin-rpc",
"-L",
f"https://github.com/Radiicall/jellyfin-rpc/releases/latest/download/{linux_binary}",
]
)
subprocess.run(
[
"chmod",
"+x",
os.environ["HOME"].removesuffix("/") + "/.local/bin/jellyfin-rpc",
]
)
if os.environ.get("XDG_CONFIG_HOME"):
path = (
os.environ["XDG_CONFIG_HOME"].removesuffix("/")
+ "/systemd/user/jellyfin-rpc.service"
)
else:
path = (
os.environ["HOME"].removesuffix("/")
+ "/.config/systemd/user/jellyfin-rpc.service"
)
autostart = confirm(
message="Do you want to autostart Jellyfin-RPC at login using systemd?", default=False
)
if autostart:
print(f"\nSetting up service file in {path}")
subprocess.run(["mkdir", "-p", path.removesuffix("jellyfin-rpc.service")])
content = f"""[Unit]
Description=Jellyfin-RPC Service
Documentation=https://github.com/Radiicall/jellyfin-rpc
After=network.target
[Service]
Type=simple
ExecStart={os.environ["HOME"].removesuffix("/") + "/.local/bin/jellyfin-rpc"}
Restart=on-failure
[Install]
WantedBy=default.target"""
file = open(path, "w")
file.write(content)
file.close()
subprocess.run(["systemctl", "--user", "daemon-reload"])
subprocess.run(
["systemctl", "--user", "enable", "--now", "jellyfin-rpc.service"]
)
print("Jellyfin-RPC is now set up to start at login.")
print("Installation complete!")