11 Commits

Author SHA1 Message Date
40429892e8 minor bump 2025-06-27 08:19:42 +01:00
1a5c0d4537 use help output in README 2025-06-27 08:19:18 +01:00
ebad2f51c9 upd screenshot 2025-06-27 08:19:08 +01:00
528573cd5a process pause + record mouse/keyboard events 2025-06-27 08:19:02 +01:00
3eb37295c2 register pause + resume as subcommands on the CLI 2025-06-27 08:17:16 +01:00
ef68915f6a implement pause + resume 2025-06-27 08:17:04 +01:00
b161c1ec3d upd description 2025-06-27 07:54:12 +01:00
e37ae8dddc upd screenshot 2025-06-27 07:50:46 +01:00
1d6fbd0bda add tabgroup to mainframe
add pause, split and add chapter buttons to Recorder tab
- they are not implemented yet

add timeouts on requests + handle them

add obs connected status message on GUI load
2025-06-27 07:47:56 +01:00
0814678278 debug should be a hidden option
patch bump
2025-06-26 14:38:53 +01:00
68041f1406 fix bug with parser (regression)
patch bump
2025-06-26 09:56:53 +01:00
9 changed files with 232 additions and 42 deletions

View File

@@ -3,9 +3,7 @@
[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
A single purpose application for naming file recording in OBS.
Run it as a CLI or a GUI.
A simple OBS recorder app. Run it as a CLI or a GUI.
---
@@ -77,8 +75,10 @@ simple-recorder --theme="Light Purple"
Usage: simple-recorder [OPTIONS] COMMAND
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ start Start recording
┃ stop Stop recording
┃ start Start recording ┃
┃ stop Stop recording ┃
┃ pause Pause recording ┃
┃ resume Resume recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -1,6 +1,6 @@
[project]
name = "simple-recorder"
version = "0.1.5"
version = "0.2.0"
description = "A simple OBS recorder"
authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }]
dependencies = [

View File

@@ -5,6 +5,8 @@ from typing_extensions import override
from .errors import SimpleRecorderError
from .gui import SimpleRecorderWindow
from .pause import Pause
from .resume import Resume
from .start import Start
from .stop import Stop
@@ -35,7 +37,7 @@ def theme_parser(value: str) -> str:
class SimpleRecorder(Command):
subcommand: Start | Stop | None = None
subcommand: Start | Stop | Pause | Resume | None = None
host: str = arg(default="localhost", env="OBS_HOST", help="OBS WebSocket host")
port: int = arg(default=4455, env="OBS_PORT", help="OBS WebSocket port")
password: str | None = arg(
@@ -47,10 +49,19 @@ class SimpleRecorder(Command):
env="OBS_THEME",
help=f"GUI theme ({', '.join(themes)})",
)
debug: bool = arg(
default=False,
env="DEBUG",
help="Enable debug logging",
hidden=True,
)
@override
async def run(self):
"""Run the Simple Recorder GUI."""
if self.debug:
logging.basicConfig(level=logging.DEBUG)
window = SimpleRecorderWindow(self.host, self.port, self.password, self.theme)
await window.run()

View File

@@ -1,8 +1,11 @@
import logging
import FreeSimpleGUI as fsg
from clypi import ClypiException
import obsws_python as obsws
from .errors import SimpleRecorderError
from .pause import Pause
from .resume import Resume
from .start import Start
from .stop import Stop
@@ -17,25 +20,89 @@ class SimpleRecorderWindow(fsg.Window):
self.password = password
fsg.theme(theme)
layout = [
[fsg.Text("Enter recording filename:")],
[fsg.InputText("", key="-FILENAME-")],
[fsg.Button("Start Recording"), fsg.Button("Stop Recording")],
[fsg.Text("Status: Not started", key="-OUTPUT-")],
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_version()
status_message = f"Connected to OBS {resp.obs_version}"
except (ConnectionRefusedError, TimeoutError):
status_message = "Failed to connect to OBS. Is it running?"
recorder_layout = [
[fsg.Text("Enter recording filename:", key="-PROMPT-")],
[fsg.InputText("default_name", key="-FILENAME-", focus=True)],
[
fsg.Button("Start", key="Start Recording", size=(20, 1)),
fsg.Button("Stop", key="Stop Recording", size=(20, 1)),
],
[
fsg.Button("Pause", key="Pause Recording", size=(20, 1)),
fsg.Button("Resume", key="Resume Recording", size=(20, 1)),
],
[
fsg.Button("Split", key="Split Recording", size=(20, 1)),
fsg.Button("Add Chapter", key="Add Chapter", size=(20, 1)),
],
]
super().__init__("Simple Recorder", layout, finalize=True)
frame = fsg.Frame(
"",
recorder_layout,
relief=fsg.RELIEF_SUNKEN,
)
recorder_tab = fsg.Tab(
"Recorder",
[
[frame],
[
fsg.Text(
f"Status: {status_message}",
key="-OUTPUT-",
text_color="white"
if status_message.startswith("Connected")
else "red",
)
],
],
)
settings_layout = [
[fsg.Text("Enter the filepath for the recording:")],
[fsg.InputText("", key="-FILEPATH-", size=(45, 1))],
]
settings_tab = fsg.Tab("Settings", settings_layout)
mainframe = [
[fsg.TabGroup([[recorder_tab, settings_tab]])],
]
super().__init__("Simple Recorder", mainframe, finalize=True)
self["-FILENAME-"].bind("<Return>", " || RETURN")
self["Start Recording"].bind("<Return>", " || RETURN")
self["Stop Recording"].bind("<Return>", " || RETURN")
self["Pause Recording"].bind("<Return>", " || RETURN")
self["Resume Recording"].bind("<Return>", " || RETURN")
self["-FILENAME-"].bind("<KeyPress>", " || KEYPRESS")
self["-FILENAME-"].update(select=True)
self["Add Chapter"].bind("<FocusIn>", " || FOCUS")
self["Add Chapter"].bind("<Enter>", " || FOCUS")
self["Add Chapter"].bind("<FocusOut>", " || LEAVE")
self["Add Chapter"].bind("<Leave>", " || LEAVE")
self["Add Chapter"].bind("<Button-3>", " || RIGHT_CLICK")
async def run(self):
while True:
event, values = self.read()
self.logger.debug(f"Event: {event}, Values: {values}")
if event == fsg.WIN_CLOSED:
break
match event.split(" || "):
case ["Start Recording", "RETURN" | None] | ["-FILENAME-", "RETURN"]:
match e := event.split(" || "):
case ["Start Recording"] | ["Start Recording" | "-FILENAME-", "RETURN"]:
try:
await Start(
filename=values["-FILENAME-"],
@@ -46,12 +113,12 @@ class SimpleRecorderWindow(fsg.Window):
self["-OUTPUT-"].update(
"Recording started successfully", text_color="green"
)
except ClypiException as e:
except SimpleRecorderError as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Stop Recording", "RETURN" | None]:
case ["Stop Recording"] | ["Stop Recording", "RETURN"]:
try:
await Stop(
host=self.host, port=self.port, password=self.password
@@ -59,12 +126,58 @@ class SimpleRecorderWindow(fsg.Window):
self["-OUTPUT-"].update(
"Recording stopped successfully", text_color="green"
)
except ClypiException as e:
except SimpleRecorderError as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Pause Recording"] | ["Pause Recording", "RETURN"]:
try:
await Pause(
host=self.host, port=self.port, password=self.password
).run()
self["-OUTPUT-"].update(
"Recording paused successfully", text_color="green"
)
except SimpleRecorderError as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Resume Recording"] | ["Resume Recording", "RETURN"]:
try:
await Resume(
host=self.host, port=self.port, password=self.password
).run()
self["-OUTPUT-"].update(
"Recording resumed successfully", text_color="green"
)
except SimpleRecorderError as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Add Chapter", "FOCUS" | "LEAVE" as focus_event]:
if focus_event == "FOCUS":
self["-OUTPUT-"].update(
"Right-click to set a chapter name", text_color="white"
)
else:
self["-OUTPUT-"].update("", text_color="white")
case ["Add Chapter", "RIGHT_CLICK"]:
_ = fsg.popup_get_text(
"Enter chapter name:",
"Add Chapter",
default_text="unnamed",
)
case ["Split Recording" | "Add Chapter"]:
self["-OUTPUT-"].update(
"This feature is not implemented yet", text_color="orange"
)
case _:
self.logger.warning(f"Unhandled event: {event}")
self.logger.debug(f"Unhandled event: {e}")
self.close()

View File

@@ -0,0 +1,30 @@
import obsws_python as obsws
from clypi import Command, arg
from typing_extensions import override
from .errors import SimpleRecorderError
class Pause(Command):
"""Pause recording."""
host: str = arg(inherited=True)
port: int = arg(inherited=True)
password: str = arg(inherited=True)
@override
async def run(self):
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if not resp.output_active:
raise SimpleRecorderError("No active recording to pause.")
if resp.output_paused:
raise SimpleRecorderError("Recording is already paused.")
client.pause_record()
print("Recording paused successfully.")
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")

View File

@@ -0,0 +1,30 @@
import obsws_python as obsws
from clypi import Command, arg
from typing_extensions import override
from .errors import SimpleRecorderError
class Resume(Command):
"""Resume recording."""
host: str = arg(inherited=True)
port: int = arg(inherited=True)
password: str = arg(inherited=True)
@override
async def run(self):
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if not resp.output_active:
raise SimpleRecorderError("No active recording to resume.")
if not resp.output_paused:
raise SimpleRecorderError("Recording is not paused.")
client.resume_record()
print("Recording resumed successfully.")
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")

View File

@@ -29,18 +29,21 @@ class Start(Command):
if not self.filename:
raise SimpleRecorderError("Recording name cannot be empty.")
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password
) as client:
resp = client.get_record_status()
if resp.output_active:
raise SimpleRecorderError("Recording is already active.")
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if resp.output_active:
raise SimpleRecorderError("Recording is already active.")
filename = f"{self.filename} {self.get_timestamp()}"
client.set_profile_parameter(
"Output",
"FilenameFormatting",
filename,
)
client.start_record()
print(f"Recording started with filename: {highlight(filename)}")
filename = f"{self.filename} {self.get_timestamp()}"
client.set_profile_parameter(
"Output",
"FilenameFormatting",
filename,
)
client.start_record()
print(f"Recording started with filename: {highlight(filename)}")
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")

View File

@@ -15,12 +15,15 @@ class Stop(Command):
@override
async def run(self):
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password
) as client:
resp = client.get_record_status()
if not resp.output_active:
raise SimpleRecorderError("Recording is not active.")
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if not resp.output_active:
raise SimpleRecorderError("Recording is not active.")
client.stop_record()
print(highlight("Recording stopped successfully."))
client.stop_record()
print(highlight("Recording stopped successfully."))
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")