1 Commits

10 changed files with 102 additions and 247 deletions

View File

@@ -3,7 +3,9 @@
[![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 simple OBS recorder app. Run it as a CLI or a GUI.
A single purpose application for naming file recording in OBS.
Run it as a CLI or a GUI.
---
@@ -28,7 +30,7 @@ pipx install simple-recorder
*with pyz*
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.
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.
## Configuration
@@ -59,38 +61,37 @@ simple-recorder
![simple-recorder](./img/simple-recorder.png)
Just enter the filename and click *Start*.
Just enter the filename and click *Start Recording*.
#### Themes
You can change the colour theme with the --theme option:
Passing flags is fine, however, for example to set the theme:
```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 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ start Start recording
┃ stop Stop recording
┃ pause Pause recording ┃
┃ resume Resume recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ start Start recording ┃
┃ stop Stop recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ --host <HOST> OBS WebSocket host
┃ --port <PORT> OBS WebSocket port
┃ --password <PASSWORD> OBS WebSocket password
┃ --theme <THEME> GUI theme (Light Purple, Neutral Blue, Reds, Sandy Beach,
┃ Kayak, Light Blue 2)
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --host <HOST> OBS WebSocket host ┃
┃ --port <PORT> OBS WebSocket port ┃
┃ --password <PASSWORD> OBS WebSocket password ┃
┃ --theme <THEME> OBS WebSocket theme
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
To launch the CLI pass any subcommand (start/stop etc...), for example:
For example:
```console
simple-recorder start "File Name"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

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

View File

@@ -5,8 +5,7 @@ from typing_extensions import override
from .errors import SimpleRecorderError
from .gui import SimpleRecorderWindow
from .pause import Pause
from .resume import Resume
from .split import Split
from .start import Start
from .stop import Stop
@@ -17,18 +16,18 @@ config = ClypiConfig(
)
configure(config)
themes = [
"Light Purple",
"Neutral Blue",
"Reds",
"Sandy Beach",
"Kayak",
"Light Blue 2",
]
def theme_parser(value: str) -> str:
"""Parse the theme argument."""
themes = [
"Light Purple",
"Neutral Blue",
"Reds",
"Sandy Beach",
"Kayak",
"Light Blue 2",
"Dark Teal1",
]
if value not in themes:
raise ClypiException(
f"Invalid theme: {value}. Available themes: {', '.join(themes)}"
@@ -37,31 +36,19 @@ def theme_parser(value: str) -> str:
class SimpleRecorder(Command):
subcommand: Start | Stop | Pause | Resume | None = None
subcommand: Start | Stop | Split | 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=f"GUI theme ({', '.join(themes)})",
)
debug: bool = arg(
default=False,
env="DEBUG",
help="Enable debug logging",
hidden=True,
default="Reds", parser=theme_parser, env="OBS_THEME", help="OBS WebSocket theme"
)
@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,11 +1,9 @@
import logging
import FreeSimpleGUI as fsg
import obsws_python as obsws
from clypi import ClypiException
from .errors import SimpleRecorderError
from .pause import Pause
from .resume import Resume
from .split import Split
from .start import Start
from .stop import Stop
@@ -20,89 +18,30 @@ class SimpleRecorderWindow(fsg.Window):
self.password = password
fsg.theme(theme)
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)],
layout = [
[fsg.Text("Enter recording filename:")],
[fsg.InputText("", key="-FILENAME-")],
[
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)),
fsg.Button("Start Recording"),
fsg.Button("Stop Recording"),
fsg.Button("Split Recording"),
],
[fsg.Text("Status: Not started", key="-OUTPUT-")],
]
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)
super().__init__("Simple Recorder", layout, 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")
self["Split Recording"].bind("<Return>", " || RETURN")
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 e := event.split(" || "):
case ["Start Recording"] | ["Start Recording" | "-FILENAME-", "RETURN"]:
match event.split(" || "):
case ["Start Recording", "RETURN" | None] | ["-FILENAME-", "RETURN"]:
try:
await Start(
filename=values["-FILENAME-"],
@@ -113,12 +52,12 @@ class SimpleRecorderWindow(fsg.Window):
self["-OUTPUT-"].update(
"Recording started successfully", text_color="green"
)
except SimpleRecorderError as e:
except ClypiException as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Stop Recording"] | ["Stop Recording", "RETURN"]:
case ["Stop Recording", "RETURN" | None]:
try:
await Stop(
host=self.host, port=self.port, password=self.password
@@ -126,58 +65,25 @@ class SimpleRecorderWindow(fsg.Window):
self["-OUTPUT-"].update(
"Recording stopped successfully", text_color="green"
)
except SimpleRecorderError as e:
except ClypiException as e:
self["-OUTPUT-"].update(
f"Error: {e.raw_message}", text_color="red"
)
case ["Pause Recording"] | ["Pause Recording", "RETURN"]:
case ["Split Recording", "RETURN" | None]:
try:
await Pause(
await Split(
host=self.host, port=self.port, password=self.password
).run()
self["-OUTPUT-"].update(
"Recording paused successfully", text_color="green"
"Recording split successfully", text_color="green"
)
except SimpleRecorderError as e:
except ClypiException 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.debug(f"Unhandled event: {e}")
self.logger.warning(f"Unhandled event: {event}")
self.close()

View File

@@ -1,30 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -0,0 +1,27 @@
import obsws_python as obsws
from clypi import Command, arg
from .errors import SimpleRecorderError
class Split(Command):
"""Split recording."""
host: str = arg(inherited=True)
port: int = arg(inherited=True)
password: str = arg(inherited=True)
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.")
if resp.output_paused:
raise SimpleRecorderError(
"Recording is paused. Please resume before splitting."
)
client.split_record_file()
print("Recording split successfully.")

View File

@@ -29,21 +29,18 @@ 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, timeout=3
) as client:
resp = client.get_record_status()
if resp.output_active:
raise SimpleRecorderError("Recording is already active.")
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.")
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?")
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)}")

View File

@@ -15,15 +15,12 @@ class Stop(Command):
@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("Recording is not active.")
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.")
client.stop_record()
print(highlight("Recording stopped successfully."))
except TimeoutError:
raise SimpleRecorderError("Failed to connect to OBS. Is it running?")
client.stop_record()
print(highlight("Recording stopped successfully."))