-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
89a35d5
commit 89ba1ff
Showing
7 changed files
with
257 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"""Provide a CLI for aiovlc.""" | ||
import logging | ||
|
||
import click | ||
|
||
from aiovlc import __version__ | ||
|
||
from .client import client | ||
|
||
SETTINGS = dict(help_option_names=["-h", "--help"]) | ||
|
||
|
||
@click.group( | ||
options_metavar="", subcommand_metavar="<command>", context_settings=SETTINGS | ||
) | ||
@click.option("--debug", is_flag=True, help="Start aiovlc in debug mode.") | ||
@click.version_option(__version__) | ||
def cli(debug: bool) -> None: | ||
"""Run aiovlc as an app for testing purposes.""" | ||
if debug: | ||
logging.basicConfig(level=logging.DEBUG) | ||
else: | ||
logging.basicConfig(level=logging.INFO) | ||
|
||
|
||
cli.add_command(client) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"""Provide a TCP client.""" | ||
import asyncio | ||
import logging | ||
from typing import Awaitable, Callable | ||
|
||
import click | ||
|
||
from aiovlc.client import Client | ||
from aiovlc.exceptions import AIOVLCError | ||
|
||
LOGGER = logging.getLogger("aiovlc") | ||
|
||
ClientFactory = Callable[[], Awaitable[Client]] | ||
|
||
|
||
@click.command(options_metavar="<options>") | ||
@click.option( | ||
"--password", | ||
prompt=True, | ||
hide_input=True, | ||
required=True, | ||
help="Password of the connection.", | ||
) | ||
@click.option( | ||
"-H", | ||
"--host", | ||
default="localhost", | ||
show_default=True, | ||
help="Host of the connection.", | ||
) | ||
@click.option( | ||
"-p", | ||
"--port", | ||
default=4212, | ||
show_default=True, | ||
type=int, | ||
help="Port of the connection.", | ||
) | ||
def client(password: str, host: str, port: int) -> None: | ||
"""Start a client.""" | ||
|
||
async def client_factory() -> Client: | ||
"""Return a client.""" | ||
vlc_client = Client(password, host=host, port=port) | ||
return vlc_client | ||
|
||
run_client(client_factory) | ||
|
||
|
||
def run_client(client_factory: ClientFactory) -> None: | ||
"""Run a client.""" | ||
LOGGER.info("Starting client") | ||
try: | ||
asyncio.run(start_client(client_factory)) | ||
except KeyboardInterrupt: | ||
pass | ||
finally: | ||
LOGGER.info("Exiting CLI") | ||
|
||
|
||
async def start_client(client_factory: ClientFactory) -> None: | ||
"""Start the client.""" | ||
vlc_client = await client_factory() | ||
|
||
async with vlc_client: | ||
while True: | ||
try: | ||
await handle_client(vlc_client) | ||
except AIOVLCError as err: | ||
LOGGER.error("Error '%s'", err) | ||
break | ||
|
||
|
||
async def handle_client(vlc_client: Client) -> None: | ||
"""Handle the client calls.""" | ||
await vlc_client.login() | ||
|
||
async for msg in vlc_client.listen(): # pragma: no cover | ||
LOGGER.debug("Received: %s", msg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
"""Provide a client for aiovlc.""" | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from types import TracebackType | ||
from typing import AsyncGenerator | ||
|
||
from .const import LOGGER | ||
from .exceptions import AuthError, CommandError, ConnectError, ConnectReadError | ||
|
||
IAC = bytes([255]) # "Interpret As Command" | ||
TERMINATOR = "\n" | ||
|
||
|
||
class Client: | ||
"""Represent a client for aiovlc.""" | ||
|
||
def __init__( | ||
self, password: str, host: str = "localhost", port: int = 4212 | ||
) -> None: | ||
"""Set up the client client.""" | ||
self.host = host | ||
self.password = password | ||
self.port = port | ||
self.reader: asyncio.StreamReader | None = None | ||
self.writer: asyncio.StreamWriter | None = None | ||
|
||
async def __aenter__(self) -> Client: | ||
"""Connect the client with context manager.""" | ||
await self.connect() | ||
return self | ||
|
||
async def __aexit__( | ||
self, exc_type: Exception, exc_value: str, traceback: TracebackType | ||
) -> None: | ||
"""Disconnect the client with context manager.""" | ||
await self.disconnect() | ||
|
||
async def connect(self) -> None: | ||
"""Connect the client.""" | ||
try: | ||
self.reader, self.writer = await asyncio.open_connection( | ||
host=self.host, port=self.port | ||
) | ||
except OSError as err: | ||
raise ConnectError(f"Failed to connect: {err}") from err | ||
|
||
async def disconnect(self) -> None: | ||
"""Disconnect the client.""" | ||
assert self.writer is not None | ||
try: | ||
self.writer.close() | ||
await self.writer.wait_closed() | ||
except OSError: | ||
pass | ||
|
||
async def read(self, read_until: str = TERMINATOR) -> str: | ||
"""Return a decoded message.""" | ||
assert self.reader is not None | ||
|
||
try: | ||
read = await self.reader.readuntil(read_until.encode("utf-8")) | ||
except asyncio.LimitOverrunError as err: | ||
raise ConnectReadError(err) from err | ||
except asyncio.IncompleteReadError as err: | ||
raise ConnectReadError(err, err.partial) from err | ||
except OSError as err: | ||
raise ConnectError(f"Failed to read: {err}") from err | ||
|
||
LOGGER.debug("Bytes read: %s", read) | ||
|
||
# Drop IAC command and read again. | ||
if IAC in read: | ||
return await self.read(read_until) | ||
return read.decode("utf-8") | ||
|
||
async def write(self, command: str) -> None: | ||
"""Write a command to the connection.""" | ||
assert self.writer is not None | ||
|
||
try: | ||
self.writer.write(command.encode("utf-8")) | ||
await self.writer.drain() | ||
except OSError as err: | ||
raise ConnectError(f"Failed to write: {err}") from err | ||
|
||
async def listen(self) -> AsyncGenerator[str, None]: | ||
"""Listen and yield a message.""" | ||
while True: | ||
message_string = await self.read() | ||
|
||
yield message_string | ||
|
||
async def login(self) -> None: | ||
"""Login.""" | ||
await self.read("Password: ") | ||
full_command = f"{self.password}\n" | ||
await self.write(full_command) | ||
for _ in range(2): | ||
command_output = (await self.read("\n")).strip("\r\n") | ||
if command_output: # discard empty line once. | ||
break | ||
parsed_output = command_output.lower() | ||
if "wrong password" in parsed_output: | ||
raise AuthError("Failed to login to VLC.") | ||
if "welcome" not in parsed_output: | ||
raise CommandError(f"Unexpected password response: {command_output}") | ||
if "> " in command_output: | ||
return | ||
# Read until prompt | ||
await self.read("> ") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"""Provide common constants.""" | ||
import logging | ||
|
||
LOGGER = logging.getLogger(__package__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
"""Provide exceptions for aiovlc.""" | ||
from __future__ import annotations | ||
|
||
|
||
class AIOVLCError(Exception): | ||
"""Represent a common error for aiovlc.""" | ||
|
||
|
||
class ConnectError(AIOVLCError): | ||
"""Represent a connection error for aiovlc.""" | ||
|
||
|
||
class ConnectReadError(ConnectError): | ||
"""The client failed to read.""" | ||
|
||
def __init__(self, error: Exception, partial_bytes: bytes | None = None) -> None: | ||
"""Set up error.""" | ||
message = f"Failed to read: {error}." | ||
|
||
if partial_bytes is not None: | ||
message = f"{message} Partial bytes read: {partial_bytes!r}" | ||
|
||
super().__init__(message) | ||
self.partial_bytes = partial_bytes | ||
|
||
|
||
class AuthError(AIOVLCError): | ||
"""Represent an authentication error.""" | ||
|
||
|
||
class CommandError(AIOVLCError): | ||
"""Represent a command error.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
|
||
click==8.0.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters