onair 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. onair-1.0.1/.cursor/rules/global.mdc +64 -0
  2. onair-1.0.1/.cursor/rules/markdown.mdc +25 -0
  3. onair-1.0.1/.github/workflows/check-tag.yml +50 -0
  4. onair-1.0.1/.github/workflows/publish.yml +59 -0
  5. onair-1.0.1/.github/workflows/quality-checks.yml +40 -0
  6. onair-1.0.1/.github/workflows/version-check.yml +61 -0
  7. onair-1.0.1/.gitignore +20 -0
  8. onair-1.0.1/.mdformat.toml +3 -0
  9. onair-1.0.1/.pre-commit-config.yaml +63 -0
  10. onair-1.0.1/AGENTS.md +13 -0
  11. onair-1.0.1/CONTRIBUTING.md +21 -0
  12. onair-1.0.1/LICENSE +21 -0
  13. onair-1.0.1/PKG-INFO +89 -0
  14. onair-1.0.1/README.md +69 -0
  15. onair-1.0.1/pyproject.toml +91 -0
  16. onair-1.0.1/src/onair/__init__.py +0 -0
  17. onair-1.0.1/src/onair/__main__.py +4 -0
  18. onair-1.0.1/src/onair/app.py +122 -0
  19. onair-1.0.1/src/onair/client/__init__.py +0 -0
  20. onair-1.0.1/src/onair/client/client.py +313 -0
  21. onair-1.0.1/src/onair/commands/__init__.py +48 -0
  22. onair-1.0.1/src/onair/commands/base.py +37 -0
  23. onair-1.0.1/src/onair/commands/countries.py +75 -0
  24. onair-1.0.1/src/onair/commands/genres.py +57 -0
  25. onair-1.0.1/src/onair/commands/help.py +35 -0
  26. onair-1.0.1/src/onair/commands/info.py +61 -0
  27. onair-1.0.1/src/onair/commands/mute.py +31 -0
  28. onair-1.0.1/src/onair/commands/next.py +38 -0
  29. onair-1.0.1/src/onair/commands/pause.py +21 -0
  30. onair-1.0.1/src/onair/commands/play.py +117 -0
  31. onair-1.0.1/src/onair/commands/previous.py +50 -0
  32. onair-1.0.1/src/onair/commands/quit.py +58 -0
  33. onair-1.0.1/src/onair/commands/resume.py +22 -0
  34. onair-1.0.1/src/onair/commands/stop.py +19 -0
  35. onair-1.0.1/src/onair/commands/unmute.py +31 -0
  36. onair-1.0.1/src/onair/commands/volume.py +70 -0
  37. onair-1.0.1/src/onair/player/__init__.py +0 -0
  38. onair-1.0.1/src/onair/player/player.py +59 -0
  39. onair-1.0.1/src/onair/utils/__init__.py +0 -0
  40. onair-1.0.1/src/onair/utils/colorize.py +56 -0
  41. onair-1.0.1/src/onair/utils/listing.py +55 -0
  42. onair-1.0.1/tests/mock_client.py +37 -0
  43. onair-1.0.1/tests/test_client.py +189 -0
  44. onair-1.0.1/tests/test_commands.py +366 -0
  45. onair-1.0.1/tests/test_player.py +40 -0
  46. onair-1.0.1/tests/test_utils.py +22 -0
  47. onair-1.0.1/uv.lock +738 -0
@@ -0,0 +1,64 @@
1
+ ---
2
+ description: Coding standards for every project generated from cookiecutter-python-seed
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Generated project standards
7
+
8
+ This is a Python project generated from cookiecutter-python-seed. Treat `pyproject.toml` as the source of truth for toolchain, Python version, and dependencies. Minimum Python version is 3.12. Use uv to lock, sync, build, and bump versions.
9
+
10
+ You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
11
+
12
+ ## Before writing code
13
+
14
+ Read the task and the code it touches, trace the real flow end to end, then stop at the first rung that holds:
15
+
16
+ 1. Does this need to be built at all? (YAGNI)
17
+ 2. Does it already exist in this codebase? Reuse it.
18
+ 3. Does the standard library already do this? Use it.
19
+ 4. Does an already-declared dependency solve it? Use it.
20
+ 5. Can this be one boring line? Make it one line.
21
+ 6. Only then: write the minimum code that works.
22
+
23
+ Bug fix = root cause, not symptom. Grep every caller of the function you touch and fix the shared function once.
24
+
25
+ ## Toolchain
26
+
27
+ - Match existing layout: `src/` for packages, `tests/` for tests, application entry points as already configured.
28
+ - Do not add a dependency unless it is declared in `pyproject.toml` (or the project's lockfile). Prefer stdlib over a new package.
29
+ - Style is enforced by Ruff. Follow `pyproject.toml`; the points below are the ones to apply consistently even when Ruff cannot.
30
+ - After editing Python files, run `poe format`.
31
+ - After editing Markdown files, run `poe format_docs`.
32
+
33
+ ## Comments and docs
34
+
35
+ - No docstrings except on base/abstract classes and methods (the contract for implementors) and on complex functions where extra explanation genuinely helps.
36
+ - No unnecessary comments. Code should be self-explanatory. If a comment is needed, explain why, not what.
37
+ - No section-divider comments inside classes or functions. Group related methods by proximity. Dividers are allowed only in test classes.
38
+
39
+ ## Language
40
+
41
+ - Type-hint every function and method. Do not nest annotations so deeply that they hurt readability.
42
+ - No unnecessary imports.
43
+ - Use f-strings for all string formatting.
44
+ - Avoid unnecessary `Path` wrappers; join with `/` on `Path` objects.
45
+ - ASCII only in identifiers, comments, and docs. No emojis. Non-ASCII letters are allowed only when the content is actually in another language (e.g. Polish, Russian).
46
+
47
+ ## Quality
48
+
49
+ - Use `poe format` / `poe format_docs` instead of hand-formatting style.
50
+ - Deletion over addition. Boring over clever. Fewest files possible. Shortest working diff wins, after you understand the problem.
51
+ - No abstractions, boilerplate, or dependencies that weren't requested.
52
+ - Question complex requests: "Do you actually need X, or does Y cover it?"
53
+ - When two stdlib approaches are the same size, pick the edge-case-correct one.
54
+ - Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n^2) scan, naive heuristic) with a `ponytail:` comment naming the ceiling and upgrade path.
55
+ - Non-trivial logic leaves one runnable check: the smallest test that fails if the logic breaks. Trivial one-liners need no new test. Run the project's test command before claiming work is done.
56
+ - Do not commit secrets, `.env` files, or local virtualenvs.
57
+
58
+ Not lazy about: understanding the problem, input validation at trust boundaries, error handling that prevents data loss, security, anything explicitly requested.
59
+
60
+ ## Agents
61
+
62
+ - Keep this file for conventions that should always apply.
63
+ - Put path-specific Cursor rules in additional `.mdc` files in this directory.
64
+ - Keep `AGENTS.md` as a short index that points here.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Markdown formatting for README, docs, and other .md files
3
+ globs: "**/*.md"
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Markdown
8
+
9
+ ASCII only. No emojis, smart quotes, or non-English letters.
10
+
11
+ Use fenced code blocks with a language tag when the snippet has a language:
12
+
13
+ ```python
14
+ from pathlib import Path
15
+
16
+ Path("data") / "out.csv"
17
+ ```
18
+
19
+ Do not leave unlabeled fences for Python, TOML, shell, or JSON. Plain fences are fine for output or prose examples with no language.
20
+
21
+ Keep heading levels, lists, and tables consistent within a file: one heading style, one list marker (`-`), one table alignment. Do not mix numbered and bulleted lists for the same kind of items.
22
+
23
+ Skip extra bold, italics, and emphasis. Highlight a term only when it would otherwise be missed.
24
+
25
+ Write short, plain sentences. If a technical term is necessary, introduce it in the same sentence.
@@ -0,0 +1,50 @@
1
+ name: "Check Tag"
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ tag:
7
+ description: Version tag like v1.2.3
8
+ required: true
9
+ type: string
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ check-tag:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+ with:
21
+ ref: refs/tags/${{ inputs.tag }}
22
+ fetch-tags: true
23
+ fetch-depth: 0
24
+
25
+ - name: Check that tag is on main
26
+ env:
27
+ TAG: ${{ inputs.tag }}
28
+ run: |
29
+ git fetch origin refs/heads/main:refs/remotes/origin/main --force
30
+ if ! git merge-base --is-ancestor HEAD refs/remotes/origin/main; then
31
+ echo "tag $TAG is not on main"
32
+ exit 1
33
+ fi
34
+
35
+ - name: Set up uv
36
+ uses: astral-sh/setup-uv@v7
37
+ with:
38
+ python-version: '3.12'
39
+ enable-cache: false
40
+
41
+ - name: Ensure tag matches version in pyproject.toml
42
+ env:
43
+ TAG: ${{ inputs.tag }}
44
+ run: |
45
+ version=$(uv version --short)
46
+ tag_version="${TAG#v}"
47
+ if [ "$version" != "$tag_version" ]; then
48
+ echo "version in pyproject.toml ($version) does not match tag ($TAG)"
49
+ exit 1
50
+ fi
@@ -0,0 +1,59 @@
1
+ name: "Publish"
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ check-tag:
13
+ uses: ./.github/workflows/check-tag.yml
14
+ with:
15
+ tag: ${{ github.ref_name }}
16
+
17
+ build:
18
+ needs: check-tag
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Set up uv
25
+ uses: astral-sh/setup-uv@v7
26
+ with:
27
+ python-version: '3.12'
28
+
29
+ - name: Build
30
+ run: uv build
31
+
32
+ - name: Upload artifact
33
+ uses: actions/upload-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+
38
+ publish:
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/onair
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - name: Download artifact
48
+ uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+
53
+ - name: Set up uv
54
+ uses: astral-sh/setup-uv@v7
55
+ with:
56
+ enable-cache: false
57
+
58
+ - name: Publish
59
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,40 @@
1
+ name: "Quality Checks"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ types: [opened, synchronize, reopened]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ quality-checks:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up uv
20
+ uses: astral-sh/setup-uv@v7
21
+ with:
22
+ python-version: '3.12'
23
+
24
+ - name: Lock dependencies
25
+ run: uv lock --check
26
+
27
+ - name: Sync dependencies
28
+ run: uv sync --locked
29
+
30
+ - name: Check code formatting
31
+ run: uv run ruff format --check .
32
+
33
+ - name: Check code linting
34
+ run: uv run ruff check .
35
+
36
+ - name: Check Markdown formatting
37
+ run: find . -name '*.md' ! -path './.venv/*' ! -path './.git/*' -print0 | xargs -0 -r uv run mdformat --check
38
+
39
+ - name: Run tests
40
+ run: uv run pytest
@@ -0,0 +1,61 @@
1
+ name: "Version Check"
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ types: [opened, synchronize, reopened]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ version-check:
13
+ runs-on: ubuntu-latest
14
+ name: Ensure version bumped
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Set up uv
22
+ uses: astral-sh/setup-uv@v7
23
+ with:
24
+ python-version: '3.12'
25
+ enable-cache: false
26
+
27
+ - name: Determine PR version
28
+ id: get-pr-version
29
+ run: echo "version=$(uv version --short)" >> "$GITHUB_OUTPUT"
30
+
31
+ - name: Determine main version
32
+ id: get-main-version
33
+ run: |
34
+ git fetch origin refs/heads/main:refs/remotes/origin/main --depth=1
35
+ git checkout -B main origin/main
36
+ main_version=$(uv version --short)
37
+ echo "Main version: $main_version"
38
+ echo "version=$main_version" >> "$GITHUB_OUTPUT"
39
+
40
+ - name: Compare versions
41
+ run: |
42
+ pr_version="${{ steps.get-pr-version.outputs.version }}"
43
+ main_version="${{ steps.get-main-version.outputs.version }}"
44
+
45
+ if [ -z "$pr_version" ] || [ -z "$main_version" ]; then
46
+ echo "Version is not set"
47
+ exit 1
48
+ fi
49
+
50
+ if [ "$pr_version" = "$main_version" ]; then
51
+ echo "Version unchanged ($pr_version). Please bump the version (e.g. poe patch)"
52
+ exit 1
53
+ fi
54
+
55
+ highest_version=$(echo -e "$pr_version\n$main_version" | sort -V | tail -n1)
56
+ if [ "$highest_version" != "$pr_version" ]; then
57
+ echo "Version $highest_version is higher than PR version $pr_version. Please bump the version (e.g. poe patch)"
58
+ exit 1
59
+ fi
60
+
61
+ echo "Version $pr_version is valid"
onair-1.0.1/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ .venv/
2
+
3
+ __pycache__/
4
+ *.py[cod]
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+
9
+ .env
10
+ .env.*
11
+
12
+ .idea/
13
+ .vscode/
14
+ .DS_Store
15
+
16
+ build/
17
+ dist/
18
+ sdist/
19
+ wheels/
20
+ *.egg-info/
@@ -0,0 +1,3 @@
1
+ wrap = "keep"
2
+ number = true
3
+ end_of_line = "lf"
@@ -0,0 +1,63 @@
1
+ minimum_pre_commit_version: '3.2.0'
2
+
3
+ default_install_hook_types:
4
+ - pre-push
5
+ - post-checkout
6
+ - post-merge
7
+
8
+ repos:
9
+ - repo: local
10
+ hooks:
11
+ - id: uv-lock-check
12
+ name: uv lock --check
13
+ description: check that uv.lock is consistent with pyproject.toml
14
+ entry: uv lock --check
15
+ language: system
16
+ pass_filenames: false
17
+ files: '^(pyproject\.toml|uv\.lock)$'
18
+ stages: [pre-push]
19
+
20
+ - id: uv-lock-check-after-git
21
+ name: uv lock --check
22
+ description: check that uv.lock is consistent with pyproject.toml
23
+ entry: uv lock --check
24
+ language: system
25
+ pass_filenames: false
26
+ always_run: true
27
+ stages: [post-checkout, post-merge]
28
+
29
+ - id: uv-sync
30
+ name: uv sync
31
+ description: sync dependencies
32
+
33
+ entry: uv sync --no-active --locked
34
+
35
+ language: system
36
+ always_run: true
37
+ pass_filenames: false
38
+ stages: [post-checkout, post-merge]
39
+ verbose: true
40
+
41
+ - id: ruff-check
42
+ name: ruff check
43
+ description: check for style issues
44
+ entry: uv run ruff check --force-exclude
45
+ language: system
46
+ pass_filenames: false
47
+ stages: [pre-push]
48
+
49
+ - id: ruff-format
50
+ name: ruff format
51
+ description: check that files are formatted
52
+ entry: uv run ruff format --check --force-exclude
53
+ language: system
54
+ pass_filenames: false
55
+ stages: [pre-push]
56
+
57
+ - id: md-format
58
+ name: md format
59
+ description: format markdown files
60
+ entry: uv run mdformat --check
61
+ language: system
62
+ files: '\.md$'
63
+ stages: [pre-push]
onair-1.0.1/AGENTS.md ADDED
@@ -0,0 +1,13 @@
1
+ # Agent instructions
2
+
3
+ This project was generated from cookiecutter-python-seed.
4
+
5
+ ## Tooling
6
+
7
+ - All AI IDEs: follow this file.
8
+ - Cursor: project rules live in [`.cursor/rules/`](.cursor/rules/). This file is the index; do not duplicate those rules here.
9
+
10
+ ## Standards
11
+
12
+ - Always-on: `.cursor/rules/global.mdc`
13
+ - Markdown (`.md` files): `.cursor/rules/markdown.mdc`
@@ -0,0 +1,21 @@
1
+ # Contributing
2
+
3
+ ```shell
4
+ uv sync
5
+ ```
6
+
7
+ Commit `uv.lock`. `poe hooks` installs git hooks (pre-push, post-checkout, post-merge).
8
+
9
+ ```shell
10
+ poe check
11
+ poe check_docs
12
+ poe test
13
+ ```
14
+
15
+ `poe format` and `poe format_docs` apply the same tools in write mode.
16
+
17
+ ## Release
18
+
19
+ Bump in the PR (`poe patch` / `minor` / `major`). CI fails if `[project].version` is not higher than on `main`.
20
+
21
+ After merge, tag `vX.Y.Z` on `main` (must match the project version) and push the tag. GitHub Actions publishes the sdist and wheel to PyPI using trusted publishing. Register this repository as a trusted publisher on PyPI before the first release.
onair-1.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Roman Kaczanowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
onair-1.0.1/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: onair
3
+ Version: 1.0.1
4
+ Summary: Interactive internet radio for the terminal
5
+ Project-URL: Homepage, https://github.com/roman-kaczanowski/onair
6
+ Project-URL: Repository, https://github.com/roman-kaczanowski/onair
7
+ Project-URL: Issues, https://github.com/roman-kaczanowski/onair/issues
8
+ Author-email: Roman Kaczanowski <roman.kaczanowski.pl@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: prompt-toolkit
17
+ Requires-Dist: python-vlc
18
+ Requires-Dist: requests
19
+ Description-Content-Type: text/markdown
20
+
21
+ # onair
22
+
23
+ [![CI](https://img.shields.io/github/actions/workflow/status/roman-kaczanowski/onair/quality-checks.yml?branch=main&style=flat-square&label=CI)](https://github.com/roman-kaczanowski/onair/actions/workflows/quality-checks.yml) [![PyPI](https://img.shields.io/pypi/v/onair?style=flat-square)](https://pypi.org/project/onair/) [![Python](https://img.shields.io/badge/python-3.12%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://docs.python.org/3/) [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE)
24
+
25
+ Turn your terminal into an internet radio. onair is a small interactive shell for finding and playing stations by genre or country.
26
+
27
+ ## Features
28
+
29
+ - Browse popular genres and countries.
30
+ - Jump between stations and return to recently played ones.
31
+ - Pause, resume, mute, and adjust volume without leaving the prompt.
32
+ - Use tab completion, command history, and short aliases.
33
+ - Discover stations through [Radio Browser](https://www.radio-browser.info/) without an API key.
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.12 or newer.
38
+ - [VLC](https://www.videolan.org/vlc/) installed on your system. The `python-vlc` binding is installed with onair, but playback still requires the VLC application and its native library.
39
+
40
+ ## Quick start
41
+
42
+ Install the release from PyPI:
43
+
44
+ ```shell
45
+ python -m pip install onair
46
+ onair
47
+ ```
48
+
49
+ Or run it without installing by using [uv](https://docs.astral.sh/uv/):
50
+
51
+ ```shell
52
+ uvx onair
53
+ ```
54
+
55
+ To run the source:
56
+
57
+ ```shell
58
+ uv sync
59
+ uv run onair
60
+ ```
61
+
62
+ Then choose a genre or country and start exploring:
63
+
64
+ ```text
65
+ onair> help
66
+ onair> play chillout
67
+ onair> info
68
+ onair> next
69
+ onair> previous
70
+ onair> country poland
71
+ onair> volume +10
72
+ onair> pause
73
+ onair> resume
74
+ onair> stop
75
+ ```
76
+
77
+ Run `help` for every command and `help play` for command-specific examples. Running `play` without a genre resumes paused playback or chooses a genre at random.
78
+
79
+ ## Acknowledgments
80
+
81
+ Inspired by cmd.fm. onair is an independent project and is not affiliated with or endorsed by cmd.fm.
82
+
83
+ ## Development
84
+
85
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and release process.
86
+
87
+ ## License
88
+
89
+ Licensed under the [MIT License](LICENSE).
onair-1.0.1/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # onair
2
+
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/roman-kaczanowski/onair/quality-checks.yml?branch=main&style=flat-square&label=CI)](https://github.com/roman-kaczanowski/onair/actions/workflows/quality-checks.yml) [![PyPI](https://img.shields.io/pypi/v/onair?style=flat-square)](https://pypi.org/project/onair/) [![Python](https://img.shields.io/badge/python-3.12%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://docs.python.org/3/) [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE)
4
+
5
+ Turn your terminal into an internet radio. onair is a small interactive shell for finding and playing stations by genre or country.
6
+
7
+ ## Features
8
+
9
+ - Browse popular genres and countries.
10
+ - Jump between stations and return to recently played ones.
11
+ - Pause, resume, mute, and adjust volume without leaving the prompt.
12
+ - Use tab completion, command history, and short aliases.
13
+ - Discover stations through [Radio Browser](https://www.radio-browser.info/) without an API key.
14
+
15
+ ## Requirements
16
+
17
+ - Python 3.12 or newer.
18
+ - [VLC](https://www.videolan.org/vlc/) installed on your system. The `python-vlc` binding is installed with onair, but playback still requires the VLC application and its native library.
19
+
20
+ ## Quick start
21
+
22
+ Install the release from PyPI:
23
+
24
+ ```shell
25
+ python -m pip install onair
26
+ onair
27
+ ```
28
+
29
+ Or run it without installing by using [uv](https://docs.astral.sh/uv/):
30
+
31
+ ```shell
32
+ uvx onair
33
+ ```
34
+
35
+ To run the source:
36
+
37
+ ```shell
38
+ uv sync
39
+ uv run onair
40
+ ```
41
+
42
+ Then choose a genre or country and start exploring:
43
+
44
+ ```text
45
+ onair> help
46
+ onair> play chillout
47
+ onair> info
48
+ onair> next
49
+ onair> previous
50
+ onair> country poland
51
+ onair> volume +10
52
+ onair> pause
53
+ onair> resume
54
+ onair> stop
55
+ ```
56
+
57
+ Run `help` for every command and `help play` for command-specific examples. Running `play` without a genre resumes paused playback or chooses a genre at random.
58
+
59
+ ## Acknowledgments
60
+
61
+ Inspired by cmd.fm. onair is an independent project and is not affiliated with or endorsed by cmd.fm.
62
+
63
+ ## Development
64
+
65
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and release process.
66
+
67
+ ## License
68
+
69
+ Licensed under the [MIT License](LICENSE).