diff --git a/aiovlc/client.py b/aiovlc/client.py index 5938b2d..fbcb536 100644 --- a/aiovlc/client.py +++ b/aiovlc/client.py @@ -7,10 +7,11 @@ from typing import Literal from .const import LOGGER -from .exceptions import AuthError, CommandError, ConnectError, ConnectReadError +from .exceptions import CommandError, ConnectError, ConnectReadError from .model.command import ( Add, Clear, + Command, Enqueue, GetLength, GetLengthOutput, @@ -19,6 +20,7 @@ Info, InfoOutput, Next, + Password, Pause, Play, Prev, @@ -114,29 +116,25 @@ async def write(self, command: str) -> None: async def login(self) -> None: """Login.""" await self.read("Password: ") - command_string = f"{self.password}\n" - await self.write(command_string) - 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: + password_output = await Password(self.password).send(self) + if "> " in password_output.response: return # Read until prompt await self.read("> ") - async def send_command(self, command_string: str) -> list[str]: + async def send_command(self, command: Command) -> list[str]: """Send a command and return the output.""" + command_string = command.build_command() + async with self._command_lock: - LOGGER.debug("Sending command: %s", command_string.strip()) + if command.log_command: + LOGGER.debug("Sending command: %s", command_string.strip()) await self.write(command_string) - command_output = (await self.read("> ")).split("\r\n")[:-1] - LOGGER.debug("Command output: %s", command_output) + raw_output = await self.read(command.read_terminator) + + command_output = raw_output.split("\r\n")[:-1] + LOGGER.debug("Command output: %s", command_output) + if command_output: if re.match( r"Unknown command `.*'\. Type `help' for help\.", command_output[0] diff --git a/aiovlc/model/command.py b/aiovlc/model/command.py index c71a1b3..7f7ff1e 100644 --- a/aiovlc/model/command.py +++ b/aiovlc/model/command.py @@ -4,19 +4,27 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Generic, Literal, TypeVar -from ..exceptions import CommandParameterError, CommandParseError +from ..exceptions import ( + AuthError, + CommandError, + CommandParameterError, + CommandParseError, +) if TYPE_CHECKING: from ..client import Client T = TypeVar("T") +DEFAULT_COMMAND_READ_TERMINATOR = "> " @dataclass class Command(Generic[T]): """Represent a VLC command.""" + log_command: bool = field(init=False, default=True) prefix: str = field(init=False) + read_terminator: str = field(init=False, default=DEFAULT_COMMAND_READ_TERMINATOR) async def send(self, client: Client) -> T: """Send the command.""" @@ -24,7 +32,7 @@ async def send(self, client: Client) -> T: async def _send(self, client: Client) -> T: """Send the command.""" - output = await client.send_command(self.build_command()) + output = await client.send_command(self) return self.parse_output(output) def build_command(self) -> str: @@ -169,6 +177,44 @@ def parse_output(self, output: list[str]) -> InfoOutput: return InfoOutput(data=data) +@dataclass +class PasswordOutput(CommandOutput): + """Represent the password command output.""" + + response: str + + +@dataclass +class Password(Command[PasswordOutput]): + """Represent the password command.""" + + log_command = False + prefix = "" + password: str + read_terminator = "\n" + + def build_command(self) -> str: + """Return the full command string.""" + return f"{self.prefix}{self.password}\n" + + def parse_output(self, output: list[str]) -> PasswordOutput: + """Parse command output.""" + response: str = "" + for line in output: + if not line: + continue + response = line + parsed_output = response.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: {response}") + + if not response: + raise CommandError("Empty password response") + return PasswordOutput(response) + + @dataclass class Next(Command[None]): """Represent the next command."""