Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ Settings are stored in `~/.config/sendspin/`:
| `use_mpris` | boolean | TUI/daemon | Enable MPRIS integration (default: true) |
| `hook_start` | string | TUI/daemon | Command to run when audio stream starts |
| `hook_stop` | string | TUI/daemon | Command to run when audio stream stops |
| `source` | string | serve | Default audio source (file path or URL) |
| `source` | string | serve | Default audio source (file path or URL, ffmpeg input) |
| `source_format` | string | serve | ffmpeg container format for audio source |
| `clients` | array | serve | Client URLs to connect to (`--client`) |

Settings are automatically saved when changed through the TUI. You can also edit the JSON file directly while the client is not running.
Expand Down
6 changes: 6 additions & 0 deletions sendspin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
default=None,
help="Audio source: local file path or URL (http/https)",
)
serve_parser.add_argument(
"--source-format",
default=None,
help="ffmpeg container format for source audio",
)
serve_parser.add_argument(
"--demo",
action="store_true",
Expand Down Expand Up @@ -392,6 +397,7 @@ async def _run_serve_mode(args: argparse.Namespace) -> int:

serve_config = ServeConfig(
source=source,
source_format=args.source_format or settings.source_format,
port=args.port,
name=args.name,
clients=args.clients or settings.clients,
Expand Down
3 changes: 2 additions & 1 deletion sendspin/serve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class ServeConfig:
"""Configuration for the serve command."""

source: str
source_format: str | None = None
port: int = 8928
name: str = "Sendspin Server"
clients: list[str] | None = None
Expand Down Expand Up @@ -220,7 +221,7 @@ def on_server_event(server: SendspinServer, event: SendspinEvent) -> None:

# Decode and stream audio
try:
audio_source = await decode_audio(config.source)
audio_source = await decode_audio(config.source, source_format=config.source_format)
media_stream = MediaStream(
main_channel_source=audio_source.generator,
main_channel_format=audio_source.format,
Expand Down
3 changes: 2 additions & 1 deletion sendspin/serve/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def decode_audio(
*,
target_sample_rate: int = 48000,
target_channels: int = 2,
source_format: str | None = None,
) -> AudioSource:
"""
Decode an audio source (file path or URL) to PCM.
Expand Down Expand Up @@ -78,7 +79,7 @@ async def pcm_generator() -> AsyncGenerator[bytes, None]:
if container is not None:
container.close()

container = av.open(source)
container = av.open(format=source_format, file=source)
resampler = av.AudioResampler(format="s16", layout=layout, rate=target_sample_rate)
for frame in container.decode(container.streams.audio[0]):
for resampled in resampler.resample(frame):
Expand Down
4 changes: 4 additions & 0 deletions sendspin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class ServeSettings(BaseSettings):
"""Settings for serve mode."""

source: str | None = None
source_format: str | None = None
clients: list[str] | None = None

def update(
Expand All @@ -210,6 +211,7 @@ def update(
log_level: str | None = None,
listen_port: int | None = None,
source: str | None = None,
source_format: str | None = None,
clients: list[str] | None = None,
) -> None:
"""Update settings fields. Only changed fields trigger a save."""
Expand All @@ -219,6 +221,7 @@ def update(
"log_level": log_level,
"listen_port": listen_port,
"source": source,
"source_format": source_format,
"clients": clients,
}
)
Expand All @@ -238,6 +241,7 @@ def _load(self) -> None:
self.log_level = data.get("log_level")
self.listen_port = data.get("listen_port")
self.source = data.get("source")
self.source_format = data.get("source_format")
self.clients = data.get("clients")
logger.info("Loaded settings from %s", self._settings_file)
except (json.JSONDecodeError, OSError) as e:
Expand Down
Loading