diff --git a/plextraktsync/cli.py b/plextraktsync/cli.py
index e8a5372237d..07603d8d262 100644
--- a/plextraktsync/cli.py
+++ b/plextraktsync/cli.py
@@ -244,6 +244,14 @@ def subdl():
pass
+@click.group()
+def launchctl():
+ """
+ Installs launchctl wrapper
+ """
+ pass
+
+
@command()
@click.option(
"--pr",
@@ -286,12 +294,24 @@ def imdb_import():
pass
+def launchctl_available():
+ import shutil
+
+ return shutil.which("launchctl") is not None
+
+
cli.add_command(bug_report)
cli.add_command(cache)
cli.add_command(clear_collections)
cli.add_command(imdb_import)
cli.add_command(info)
cli.add_command(inspect)
+if launchctl_available():
+ cli.add_command(launchctl)
+ from .commands.launchctl import load, unload
+
+ launchctl.add_command(load)
+ launchctl.add_command(unload)
cli.add_command(login)
cli.add_command(plex_login)
if enable_self_update():
diff --git a/plextraktsync/com.github.plextraktsync.watch.plist b/plextraktsync/com.github.plextraktsync.watch.plist
new file mode 100644
index 00000000000..a9ed0cdb121
--- /dev/null
+++ b/plextraktsync/com.github.plextraktsync.watch.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ Label
+ com.github.plextraktsync.watch
+ ProgramArguments
+
+ plextraktsync
+ watch
+
+ WorkingDirectory
+ /
+ LimitLoadToSessionType
+ Aqua
+ RunAtLoad
+
+ ExitTimeOut
+ 0
+ ProcessType
+ Background
+
+
+ StandardErrorPath
+ /tmp/plextraktsync.err
+ StandardOutPath
+ /tmp/plextraktsync.out
+
+
diff --git a/plextraktsync/commands/launchctl.py b/plextraktsync/commands/launchctl.py
new file mode 100644
index 00000000000..b39f93fa477
--- /dev/null
+++ b/plextraktsync/commands/launchctl.py
@@ -0,0 +1,82 @@
+import click
+
+from plextraktsync.decorators.cached_property import cached_property
+
+
+class Plist:
+ plist_file = "com.github.plextraktsync.watch.plist"
+
+ def load(self, plist_path: str):
+ from os import system
+
+ system(f"launchctl load {plist_path}")
+
+ def unload(self, plist_path: str):
+ from os import system
+ from os.path import exists
+
+ # Skip if file does not exist.
+ if not exists(plist_path):
+ return
+ system(f"launchctl unload {plist_path}")
+
+ def create(self, plist_path: str):
+ from plextraktsync.util.packaging import program_path
+
+ with open(self.plist_default_path, encoding='utf-8') as f:
+ contents = "".join(f.readlines())
+
+ def encode(f):
+ return f'{f}'
+
+ program = "\n".join(map(encode, program_path().split(' ')))
+ contents = contents.replace('plextraktsync', program)
+ with open(plist_path, "w+") as fw:
+ fw.writelines(contents)
+
+ def remove(self, plist_path: str):
+ from os import unlink
+ from os.path import exists
+
+ # Skip if file does not exist.
+ if not exists(plist_path):
+ return
+ unlink(plist_path)
+
+ @cached_property
+ def plist_default_path(self):
+ from os.path import join
+
+ from plextraktsync.path import module_path
+
+ return join(module_path, self.plist_file)
+
+ @cached_property
+ def plist_path(self):
+ from os.path import expanduser
+
+ return expanduser(f'~/Library/LaunchAgents/{self.plist_file}')
+
+
+@click.command()
+def load():
+ """
+ Load the service.
+ """
+ p = Plist()
+ p.create(p.plist_path)
+ click.echo(f"Created: {p.plist_path}")
+ p.load(p.plist_path)
+ click.echo(f"Loaded: {p.plist_path}")
+
+
+@click.command()
+def unload():
+ """
+ Unload the service.
+ """
+ p = Plist()
+ p.unload(p.plist_path)
+ click.echo(f"Unloaded: {p.plist_path}")
+ p.remove(p.plist_path)
+ click.echo(f"Removed: {p.plist_path}")
diff --git a/plextraktsync/path.py b/plextraktsync/path.py
index 545f8c5fb2e..b800c3427c5 100644
--- a/plextraktsync/path.py
+++ b/plextraktsync/path.py
@@ -59,6 +59,7 @@ def ensure_dir(directory):
config_dir = p.config_dir
log_dir = p.log_dir
+module_path = p.module_path
default_config_file = p.default_config_file
config_file = p.config_file
config_yml = p.config_yml
diff --git a/plextraktsync/util/packaging.py b/plextraktsync/util/packaging.py
index a21073ec3ca..33c77dce0ad 100644
--- a/plextraktsync/util/packaging.py
+++ b/plextraktsync/util/packaging.py
@@ -59,6 +59,17 @@ def pipx_installed(package: str):
return package
+def program_path():
+ """
+ Return path to currently executed script
+ """
+ import sys
+
+ absdir = dirname(dirname(dirname(__file__)))
+
+ return f"env PYTHONPATH={absdir} {sys.executable} -m plextraktsync"
+
+
def program_name():
"""
Return current program name: