mirror of
https://github.com/onyx-and-iris/simple-recorder.git
synced 2025-06-27 01:40:23 +01:00
first commit
This commit is contained in:
commit
3ae3c02e8b
165
.gitignore
vendored
Normal file
165
.gitignore
vendored
Normal 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
59
README.md
Normal 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
|
||||
|
||||

|
||||
|
||||
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
BIN
img/simple-recorder.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
98
pdm.lock
generated
Normal file
98
pdm.lock
generated
Normal 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
27
pyproject.toml
Normal 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 ."
|
3
src/simple_recorder/__init__.py
Normal file
3
src/simple_recorder/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .app import run
|
||||
|
||||
__all__ = ["run"]
|
149
src/simple_recorder/app.py
Normal file
149
src/simple_recorder/app.py
Normal 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
0
tests/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user