first commit

This commit is contained in:
onyx-and-iris 2025-06-25 14:38:54 +01:00
commit 3ae3c02e8b
8 changed files with 501 additions and 0 deletions

165
.gitignore vendored Normal file
View File

@ -0,0 +1,165 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm-project.org/#use-with-ide
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.envrc
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
bin/

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# simple-recorder
A single purpose application for naming a file recording in OBS.
Run it as a CLI or a GUI.
## Requirements
- Python 3.11 or greater
## Installation
*with uv*
```console
uv tool install simple-recorder
```
*with pipx*
```console
pipx install simple-recorder
```
## Use
Without passing a subcommand (start/stop) a GUI will be launched, otherwise a CLI will be launched.
### GUI
![simple-recorder](./img/simple-recorder.png)
Just enter the filename and click *Start Recording*.
### CLI
```shell
Usage: simple-recorder [OPTIONS] COMMAND
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ start Start recording ┃
┃ stop Stop recording ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --host <HOST>
┃ --port <PORT>
┃ --password <PASSWORD>
┃ --theme <THEME>
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
```console
simple-recorder start "File Name"
simple-recorder stop
```
If no filename is passed to start then you will be prompted for one. A default_name will be used if none is supplied to the prompt.

BIN
img/simple-recorder.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

98
pdm.lock generated Normal file
View File

@ -0,0 +1,98 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:7d4367f16062b5a2d661a207b7b33518aaea59371e239b8cf90a5b5b583e1abc"
[[metadata.targets]]
requires_python = ">=3.11"
[[package]]
name = "clypi"
version = "1.8.1"
requires_python = ">=3.11"
summary = "Your all-in-one for beautiful, lightweight, prod-ready CLIs"
groups = ["default"]
dependencies = [
"python-dateutil>=2.9.0.post0",
"typing-extensions>=4.4.0",
]
files = [
{file = "clypi-1.8.1-py3-none-any.whl", hash = "sha256:7218807e31f698fe5585c63d397645e481ca846d1491a4f1fde017c7892dbe3c"},
{file = "clypi-1.8.1.tar.gz", hash = "sha256:9efa0a5a0e3668dd390e0d90321587dcb8eea12e28facd2ac437383f0de3dc76"},
]
[[package]]
name = "freesimplegui"
version = "5.2.0.post1"
summary = "The free-forever Python GUI framework."
groups = ["default"]
files = [
{file = "freesimplegui-5.2.0.post1-py3-none-any.whl", hash = "sha256:3d61eb519324503232f86b2f1bd7f5c6813ce225f6e189d0fd737ddb036af4d5"},
{file = "freesimplegui-5.2.0.post1.tar.gz", hash = "sha256:e58a0e6758e9a9e87152256911f94fcc3998356d1309973a9f4d9df2dc55f98a"},
]
[[package]]
name = "obsws-python"
version = "1.7.2"
requires_python = ">=3.9"
summary = "A Python SDK for OBS Studio WebSocket v5.0"
groups = ["default"]
dependencies = [
"tomli>=2.0.1; python_version < \"3.11\"",
"websocket-client",
]
files = [
{file = "obsws_python-1.7.2-py3-none-any.whl", hash = "sha256:acda31852ad9d7165de915b0603c13f6df527d3f61619970bf5fb562e300bc85"},
{file = "obsws_python-1.7.2.tar.gz", hash = "sha256:b5cdaad30fbe1f6d4787b6530048b9882f070c3ee7830abb6dad4a47f84d7fa0"},
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
summary = "Extensions to the standard Python datetime module"
groups = ["default"]
dependencies = [
"six>=1.5",
]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[[package]]
name = "six"
version = "1.17.0"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
summary = "Python 2 and 3 compatibility utilities"
groups = ["default"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "typing-extensions"
version = "4.14.0"
requires_python = ">=3.9"
summary = "Backported and Experimental Type Hints for Python 3.9+"
groups = ["default"]
files = [
{file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"},
{file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"},
]
[[package]]
name = "websocket-client"
version = "1.8.0"
requires_python = ">=3.8"
summary = "WebSocket client for Python with low level API options"
groups = ["default"]
files = [
{file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"},
{file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"},
]

27
pyproject.toml Normal file
View File

@ -0,0 +1,27 @@
[project]
name = "simple-recorder"
version = "0.1.0"
description = "A simple OBS recorder"
authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }]
dependencies = [
"clypi>=1.8.1",
"FreeSimpleGUI>=5.2.0.post1",
"obsws-python>=1.7.2",
]
requires-python = ">=3.11"
readme = "README.md"
license = { text = "MIT" }
[project.scripts]
simple-recorder = "simple_recorder:run"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm]
distribution = true
[tool.pdm.scripts]
compile = "uvx shiv -c simple-recorder -o bin/simple-recorder-bin ."

View File

@ -0,0 +1,3 @@
from .app import run
__all__ = ["run"]

149
src/simple_recorder/app.py Normal file
View File

@ -0,0 +1,149 @@
import logging
from datetime import datetime
import FreeSimpleGUI as fsg
import obsws_python as obsws
from clypi import ClypiConfig, ClypiException, Command, Positional, arg, configure
from typing_extensions import override
logger = logging.getLogger(__name__)
config = ClypiConfig(
nice_errors=(ClypiException,),
)
configure(config)
class Start(Command):
"""Start recording."""
filename: Positional[str] = arg(
default="default_name",
help="Name of the recording",
prompt="Enter the name for the recording",
)
host: str = arg(inherited=True)
port: int = arg(inherited=True)
password: str = arg(inherited=True)
@staticmethod
def get_timestamp():
return datetime.now().strftime("%Y-%m-%d %H-%M-%S")
@override
async def run(self):
if not self.filename:
raise ClypiException("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 ClypiException("Recording is already active.")
client.set_profile_parameter(
"Output",
"FilenameFormatting",
f"{self.filename} {self.get_timestamp()}",
)
client.start_record()
class Stop(Command):
"""Stop recording."""
host: str = arg(inherited=True)
port: int = arg(inherited=True)
password: str = arg(inherited=True)
@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 ClypiException("Recording is not active.")
client.stop_record()
def theme_parser(value: str) -> str:
"""Parse the theme argument."""
themes = ["Light Purple", "Neutral Blue", "Reds", "Sandy Beach"]
if value not in themes:
raise ClypiException(
f"Invalid theme: {value}. Available themes: {', '.join(themes)}"
)
return value
class SimpleRecorder(Command):
subcommand: Start | Stop | None = None
host: str = arg(default="localhost", env="OBS_HOST")
port: int = arg(default=4455, env="OBS_PORT")
password: str | None = arg(default=None, env="OBS_PASSWORD")
theme: str = arg(default="Reds", parser=theme_parser, env="OBS_THEME")
@override
async def run(self):
fsg.theme(self.theme)
input_text = fsg.InputText("", key="-FILENAME-")
start_record_button = fsg.Button("Start Recording", key="Start Recording")
stop_record_button = fsg.Button("Stop Recording", key="Stop Recording")
layout = [
[fsg.Text("Enter recording filename:")],
[input_text],
[start_record_button, stop_record_button],
[fsg.Text("Status: Not started", key="-OUTPUT-")],
]
window = fsg.Window("Simple Recorder", layout, finalize=True)
status_text = window["-OUTPUT-"]
input_text.bind("<Return>", "-ENTER-")
start_record_button.bind("<Return>", "-ENTER-")
stop_record_button.bind("<Return>", "-ENTER-")
while True:
event, values = window.read()
logger.debug(f"Event: {event}, Values: {values}")
if event == fsg.WIN_CLOSED:
break
elif event in (
"Start Recording",
"Start Recording-ENTER-",
"-FILENAME--ENTER-",
):
try:
await Start(
filename=input_text.get(),
host=self.host,
port=self.port,
password=self.password,
).run()
status_text.update("Status: Recording started", text_color="green")
except ClypiException as e:
status_text.update(str(e), text_color="red")
logger.error(f"Error starting recording: {e}")
elif event in ("Stop Recording", "Stop Recording-ENTER-"):
try:
await Stop(
host=self.host,
port=self.port,
password=self.password,
).run()
status_text.update("Status: Recording stopped", text_color="green")
except ClypiException as e:
status_text.update(str(e), text_color="red")
logger.error(f"Error stopping recording: {e}")
def run():
"""Run the CLI application."""
SimpleRecorder.parse().start()
if __name__ == "__main__":
run()

0
tests/__init__.py Normal file
View File