Skip to content

Commit

Permalink
Add channel pick and input pick commands (#80)
Browse files Browse the repository at this point in the history
These commands make use of pzp - an alternative to fzf written in Python, for
making it easy to search through and pick a channel or input, instead of having
to first `list` the channels/inputs and then `set` the desired one.
  • Loading branch information
Tenzer authored Dec 8, 2024
1 parent 254c0c0 commit b82f733
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/release-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v4
- name: Checkout the code
uses: actions/checkout@v4

- name: Install Poetry
run: pipx install poetry
Expand Down
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
rev: v0.8.2
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
18 changes: 16 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ classifiers = [
python = "^3.9"
cfgs = ">= 0.13.0"
getmac = ">= 0.9.0"
pzp = ">=0.0.23"
rich = ">= 13.0.0"
typer = ">= 0.15.0"
wakeonlan = ">= 2.0.0"
Expand Down
17 changes: 17 additions & 0 deletions src/alga/cli_channel.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Annotated

from pzp import pzp # type: ignore[import-untyped]
from rich import print
from rich.console import Console
from rich.table import Table
from typer import Argument, Typer

from alga import client
from alga.types import Channel


app = Typer(no_args_is_help=True, help="TV channels")
Expand Down Expand Up @@ -59,6 +61,21 @@ def list() -> None:
console.print(table)


@app.command()
def pick() -> None:
"""Show picker for selecting a channel."""

response = client.request("ssap://tv/getChannelList")
channels = []

for channel in response["channelList"]:
channels.append(Channel(channel))

channel = pzp(candidates=channels, fullscreen=False, layout="reverse")
if channel:
client.request("ssap://tv/openChannel", {"channelId": channel.id_})


@app.command()
def set(value: Annotated[str, Argument()]) -> None:
"""Change to specific channel"""
Expand Down
17 changes: 17 additions & 0 deletions src/alga/cli_input.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Annotated

from pzp import pzp # type: ignore[import-untyped]
from rich.console import Console
from rich.table import Table
from typer import Argument, Typer

from alga import client
from alga.types import InputDevice


app = Typer(no_args_is_help=True, help="HDMI and similar inputs")
Expand All @@ -31,6 +33,21 @@ def list() -> None:
console.print(table)


@app.command()
def pick() -> None:
"""Show picker for selecting an input."""

response = client.request("ssap://tv/getExternalInputList")
input_devices = []

for input_device in response["devices"]:
input_devices.append(InputDevice(input_device))

input_device = pzp(candidates=input_devices, fullscreen=False, layout="reverse")
if input_device:
client.request("ssap://tv/switchInput", {"inputId": input_device.id_})


@app.command()
def set(value: Annotated[str, Argument()]) -> None:
"""Switch to given input"""
Expand Down
30 changes: 30 additions & 0 deletions src/alga/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from dataclasses import dataclass
from typing import Any


@dataclass
class Channel:
id_: str
number: str
name: str

def __init__(self, channel: dict[str, Any]) -> None:
self.id_ = channel["channelId"]
self.number = channel["channelNumber"]
self.name = channel["channelName"]

def __str__(self) -> str:
return f"{self.number}: {self.name}"


@dataclass
class InputDevice:
id_: str
name: str

def __init__(self, input_device: dict[str, Any]) -> None:
self.id_ = input_device["id"]
self.name = input_device["label"]

def __str__(self) -> str:
return f"{self.name} ({self.id_})"
40 changes: 39 additions & 1 deletion tests/test_cli_channel.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call, patch

from faker import Faker
from typer.testing import CliRunner

from alga.__main__ import app
from alga.types import Channel


runner = CliRunner()
Expand Down Expand Up @@ -103,3 +104,40 @@ def test_list(faker: Faker, mock_request: MagicMock) -> None:
+ 1 # table footer
+ 1 # trailing newline
)


def test_pick(faker: Faker, mock_request: MagicMock) -> None:
return_value = {
"channelList": [
{
"channelId": faker.pystr(),
"channelName": faker.pystr(),
"channelNumber": f"{faker.pyint()}",
},
{
"channelId": faker.pystr(),
"channelName": faker.pystr(),
"channelNumber": f"{faker.pyint()}",
},
{
"channelId": faker.pystr(),
"channelName": faker.pystr(),
"channelNumber": f"{faker.pyint()}",
},
]
}
mock_request.return_value = return_value
first_channel = return_value["channelList"][0]

with patch("alga.cli_channel.pzp") as mock_pzp:
mock_pzp.return_value = Channel(first_channel)

result = runner.invoke(app, ["channel", "pick"])

mock_request.assert_has_calls(
[
call("ssap://tv/getChannelList"),
call("ssap://tv/openChannel", {"channelId": first_channel["channelId"]}),
]
)
assert result.exit_code == 0
28 changes: 27 additions & 1 deletion tests/test_cli_input.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call, patch

from faker import Faker
from typer.testing import CliRunner

from alga.__main__ import app
from alga.types import InputDevice


runner = CliRunner()
Expand Down Expand Up @@ -43,3 +44,28 @@ def test_list(faker: Faker, mock_request: MagicMock) -> None:
+ 1 # table footer
+ 1 # trailing newline
)


def test_pick(faker: Faker, mock_request: MagicMock) -> None:
return_value = {
"devices": [
{"id": faker.pystr(), "label": faker.pystr()},
{"id": faker.pystr(), "label": faker.pystr()},
{"id": faker.pystr(), "label": faker.pystr()},
]
}
mock_request.return_value = return_value
first_input = return_value["devices"][0]

with patch("alga.cli_input.pzp") as mock_pzp:
mock_pzp.return_value = InputDevice(first_input)

result = runner.invoke(app, ["input", "pick"])

mock_request.assert_has_calls(
[
call("ssap://tv/getExternalInputList"),
call("ssap://tv/switchInput", {"inputId": first_input["id"]}),
]
)
assert result.exit_code == 0
21 changes: 21 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from faker import Faker

from alga.types import Channel, InputDevice


def test_channel(faker: Faker) -> None:
channel = Channel(
{
"channelId": faker.pystr(),
"channelNumber": faker.pystr(),
"channelName": faker.pystr(),
}
)

assert str(channel) == f"{channel.number}: {channel.name}"


def test_input_device(faker: Faker) -> None:
input_device = InputDevice({"id": faker.pystr(), "label": faker.pystr()})

assert str(input_device) == f"{input_device.name} ({input_device.id_})"
30 changes: 30 additions & 0 deletions usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ $ alga channel [OPTIONS] COMMAND [ARGS]...
* `current`: Get the current channel
* `down`: Change channel down
* `list`: List available channels
* `pick`: Show picker for selecting a channel.
* `set`: Change to specific channel
* `up`: Change channel up

Expand Down Expand Up @@ -214,6 +215,20 @@ $ alga channel list [OPTIONS]

* `--help`: Show this message and exit.

### `alga channel pick`

Show picker for selecting a channel.

**Usage**:

```console
$ alga channel pick [OPTIONS]
```

**Options**:

* `--help`: Show this message and exit.

### `alga channel set`

Change to specific channel
Expand Down Expand Up @@ -263,6 +278,7 @@ $ alga input [OPTIONS] COMMAND [ARGS]...
**Commands**:

* `list`: List available inputs
* `pick`: Show picker for selecting an input.
* `set`: Switch to given input

### `alga input list`
Expand All @@ -279,6 +295,20 @@ $ alga input list [OPTIONS]

* `--help`: Show this message and exit.

### `alga input pick`

Show picker for selecting an input.

**Usage**:

```console
$ alga input pick [OPTIONS]
```

**Options**:

* `--help`: Show this message and exit.

### `alga input set`

Switch to given input
Expand Down

0 comments on commit b82f733

Please sign in to comment.