19 Commits

Author SHA1 Message Date
3c77be2ff9 reword 2025-06-27 08:28:06 +01:00
e8d0fcf56d upd button name in README 2025-06-27 08:25:00 +01:00
69a0f607e4 upd screenshot 2025-06-27 08:23:27 +01:00
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
bba2361964 add available themes to --theme help string
patch bump
2025-06-26 08:56:32 +01:00
d8cdae61a9 upd --help output in README 2025-06-26 06:54:47 +01:00
a43813fc00 fix help string for theme option 2025-06-26 06:53:36 +01:00
87dbd0b8e5 typo 2025-06-26 06:52:20 +01:00
00dbe43479 patch bump 2025-06-26 06:47:50 +01:00
9 changed files with 258 additions and 66 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.
---
@@ -30,7 +28,7 @@ pipx install simple-recorder
*with pyz*
An executable pyz has been included in [Release](https://github.com/onyx-and-iris/simple-recorder/releases) which you can run in Windows. Follow the steps in this [Setting up Windows for Zipapps](https://jhermann.github.io/blog/python/deployment/2020/02/29/python_zippapps_on_windows.html#Setting-Up-Windows-10-for-Zipapps) guide.
An executable pyz has been included in [Releases](https://github.com/onyx-and-iris/simple-recorder/releases) which you can run in Windows. Follow the steps in this [Setting up Windows for Zipapps](https://jhermann.github.io/blog/python/deployment/2020/02/29/python_zippapps_on_windows.html#Setting-Up-Windows-10-for-Zipapps) guide.
## Configuration
@@ -61,37 +59,38 @@ simple-recorder
![simple-recorder](./img/simple-recorder.png)
Just enter the filename and click *Start Recording*.
Just enter the filename and click *Start*.
#### Themes
Passing flags is fine, however, for example to set the theme:
You can change the colour theme with the --theme option:
```console
simple-recorder --theme="Light Purple"
```
Available themes: Light Purple, Neutral Blue, Reds, Sandy Beach, Kayak, Light Blue 2, Dark Teal1
### CLI
```shell
Usage: simple-recorder [OPTIONS] COMMAND
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ start Start recording ┃
┃ stop Stop recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┃ pause Pause recording ┃
┃ resume Resume recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ --host <HOST> OBS WebSocket host ┃
┃ --port <PORT> OBS WebSocket port ┃
┃ --password <PASSWORD> OBS WebSocket password ┃
┃ --theme <THEME> OBS WebSocket theme
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┃ --theme <THEME> GUI theme (Light Purple, Neutral Blue, Reds, Sandy Beach,
┃ Kayak, Light Blue 2)
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
For example:
To launch the CLI pass any subcommand (start/stop etc...), for example:
```console
simple-recorder start "File Name"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,6 +1,6 @@
[project]
name = "simple-recorder"
version = "0.1.3"
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
@@ -15,9 +17,6 @@ config = ClypiConfig(
)
configure(config)
def theme_parser(value: str) -> str:
"""Parse the theme argument."""
themes = [
"Light Purple",
"Neutral Blue",
@@ -25,8 +24,11 @@ def theme_parser(value: str) -> str:
"Sandy Beach",
"Kayak",
"Light Blue 2",
"Dark Teal1",
]
def theme_parser(value: str) -> str:
"""Parse the theme argument."""
if value not in themes:
raise ClypiException(
f"Invalid theme: {value}. Available themes: {', '.join(themes)}"
@@ -35,19 +37,31 @@ 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(
default=None, env="OBS_PASSWORD", help="OBS WebSocket password"
)
theme: str = arg(
default="Reds", parser=theme_parser, env="OBS_THEME", help="OBS WebSocket theme"
default="Reds",
parser=theme_parser,
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,8 +29,9 @@ class Start(Command):
if not self.filename:
raise SimpleRecorderError("Recording name cannot be empty.")
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if resp.output_active:
@@ -44,3 +45,5 @@ class Start(Command):
)
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,8 +15,9 @@ class Stop(Command):
@override
async def run(self):
try:
with obsws.ReqClient(
host=self.host, port=self.port, password=self.password
host=self.host, port=self.port, password=self.password, timeout=3
) as client:
resp = client.get_record_status()
if not resp.output_active:
@@ -24,3 +25,5 @@ class Stop(Command):
client.stop_record()
print(highlight("Recording stopped successfully."))
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")