Compare commits

..

2 Commits

3 changed files with 104 additions and 7 deletions

85
rewind/autostart.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import os
import platform
import shutil
import subprocess
from pathlib import Path
APP_NAME = "rewind-daemon"
def _find_executable():
path = shutil.which(APP_NAME)
if not path:
raise RuntimeError(f"{APP_NAME} not found in PATH")
return path
def _run(cmd: list[str]):
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{result.stderr}")
return result
def _install_linux(program_path: str):
systemd_dir = Path.home() / ".config/systemd/user"
systemd_dir.mkdir(parents=True, exist_ok=True)
obs_service = systemd_dir / "obs.service"
daemon_service = systemd_dir / "rewind-daemon.service"
if obs_service.exists():
print("obs.service already exists, skipping creation")
else:
obs_service.write_text("""[Unit]
Description=OBS Studio
After=graphical-session.target
Wants=graphical-session.target
[Service]
ExecStart=obs --minimize-to-tray
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
""")
if daemon_service.exists():
print("rewind-daemon.service already exists, skipping creation")
else:
daemon_service.write_text(f"""[Unit]
Description=Rewind Daemon
After=obs.service
Requires=obs.service
[Service]
ExecStart={program_path}
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=default.target
""")
_run(["systemctl", "--user", "daemon-reload"])
_run(["systemctl", "--user", "enable", "obs.service"])
_run(["systemctl", "--user", "enable", "rewind-daemon.service"])
_run(["systemctl", "--user", "start", "obs.service"])
_run(["systemctl", "--user", "start", "rewind-daemon.service"])
print("Installed and started OBS + rewind-daemon via systemd")
def install():
system = platform.system()
program_path = _find_executable()
if system == "Linux":
_install_linux(program_path)
else:
raise RuntimeError(f"Autostart on OS {system} not compatible or not implemented")
if __name__ == "__main__":
install()

View File

@@ -3,6 +3,7 @@ import sys
import argparse import argparse
from rewind.core import clip, mark, save, print_markers from rewind.core import clip, mark, save, print_markers
from rewind.autostart import install
def build_clip_parser(subparsers: argparse._SubParsersAction) -> None: def build_clip_parser(subparsers: argparse._SubParsersAction) -> None:
clip_parser = subparsers.add_parser("clip", help="Clips the last 'x' seconds") clip_parser = subparsers.add_parser("clip", help="Clips the last 'x' seconds")
@@ -41,6 +42,13 @@ def build_mark_parser(subparsers: argparse._SubParsersAction) -> None:
def build_list_parser(subparsers: argparse._SubParsersAction) -> None: def build_list_parser(subparsers: argparse._SubParsersAction) -> None:
list_parser = subparsers.add_parser("list", help="List all markers in the recording") list_parser = subparsers.add_parser("list", help="List all markers in the recording")
def build_install_parser(subparsers: argparse._SubParsersAction):
subparsers.add_parser(
"install",
help="Enable background recording daemon",
description="Install and enable the rewind daemon using system autostart."
)
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="rewind", prog="rewind",
@@ -53,6 +61,7 @@ def build_parser() -> argparse.ArgumentParser:
build_mark_parser(sub) build_mark_parser(sub)
build_list_parser(sub) build_list_parser(sub)
build_save_parser(sub) build_save_parser(sub)
build_install_parser(sub)
return parser return parser
@@ -69,6 +78,8 @@ def main(argv=None) -> int:
print_markers() print_markers()
elif args.command == "save": elif args.command == "save":
save(args.start, args.end) save(args.start, args.end)
elif args.command == "install":
install()
else: else:
parser.error("Unknown command") parser.error("Unknown command")
except Exception as e: except Exception as e:

View File

@@ -8,23 +8,24 @@ import logging
import shutil import shutil
import signal import signal
from pathlib import Path
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from rewind.paths import load_config from rewind.paths import load_config, get_state_dir
from rewind.core import mark, marker_exists, remove_marker from rewind.core import mark, marker_exists, remove_marker
from rewind.state import add_file_to_state, create_state_file_if_needed, cleanup_state from rewind.state import add_file_to_state, create_state_file_if_needed, cleanup_state
running = True running = True
LOG_FILE = get_state_dir() / "daemon.log"
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s:%(name)s:%(message)s")
logging.getLogger("obsws_python").setLevel(logging.CRITICAL)
INTERVAL = 10 INTERVAL = 10
SENTINEL_FILE = os.path.expanduser("~/.config/obs-studio/.sentinel") SENTINEL_FILE = os.path.expanduser("~/.config/obs-studio/.sentinel")
OBS_MAX_RETRIES = 10 OBS_MAX_RETRIES = 10
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s:%(name)s:%(message)s", handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()])
logging.getLogger("obsws_python").setLevel(logging.CRITICAL)
def shutdown(signum, frame): def shutdown(signum, frame):
global running global running
logger.info(f"Received signal {signum}, shutting down cleanly") logger.info(f"Received signal {signum}, shutting down cleanly")