diff --git a/README.rst b/README.rst index 47834ae..87eeea9 100644 --- a/README.rst +++ b/README.rst @@ -46,10 +46,10 @@ Quckstart ---------- At first we need to determine the name of the chromecast device we want to cast to. To do so, run ``lolcatt --scan``. +A default device and device aliases can be set in the ``catt`` configuration file ``~/.config/catt/config.cfg``. See catt_'s documentation for more information. To start the UI, run ``lolcatt -d ''`` (or simply ``lolcatt`` if a default device is set). -A default device and device aliases can be set in the ``catt`` configuration file (per default under ``~/.config/catt/config.cfg``), see catt_'s documentation for more information. -To cast, paste either a URL or a path to a local file into the input field and press enter. To add a URL or path to the playback queue instead of playing immediately, hit Ctrl+s instead of enter. To view and navigate in the queue, tap the name of the currently playing item. To seek, tap the progress bar. +To cast, paste either a URL or a path to a local file into the input field and press enter. To add a URL or path to the playback queue instead of playing immediately, hit Ctrl+s instead of enter. To view and navigate in the queue, tap the name of the currently playing item. To seek, tap the progress bar. To change chromecast device, tap the name of the currently active device (currently only devices with set aliases can be selected in this way). For URLs, all websites supported by yt-dlp_ (which handles media download under the hood) are supported. Find a list of supported websites here_. For local media, most common video and image formats are supported. diff --git a/lolcatt/__init__.py b/lolcatt/__init__.py index 67fd684..264a0cc 100644 --- a/lolcatt/__init__.py +++ b/lolcatt/__init__.py @@ -2,4 +2,4 @@ __author__ = """Lukas Lüftinger""" __email__ = 'lukas.lueftinger@outlook.com' -__version__ = '0.4.1' +__version__ = '0.5.0' diff --git a/lolcatt/app.py b/lolcatt/app.py index b412d9d..02f13fd 100644 --- a/lolcatt/app.py +++ b/lolcatt/app.py @@ -7,6 +7,7 @@ from lolcatt.casting.caster import Caster from lolcatt.ui.lolcatt_controls import LolCattControls from lolcatt.ui.lolcatt_device_info import LolCattDeviceInfo +from lolcatt.ui.lolcatt_devicelist import LolCattDevicelist from lolcatt.ui.lolcatt_playback_info import LolCattPlaybackInfo from lolcatt.ui.lolcatt_playlist import LolCattPlaylist from lolcatt.ui.lolcatt_progress import LolCattProgress @@ -34,6 +35,13 @@ def compose(self): yield LolCattPlaylist() +class DeviceScreen(Screen): + """A screen for the device info UI.""" + + def compose(self): + yield LolCattDevicelist() + + class LolCatt(App): """The main application class for lolcatt.""" @@ -44,13 +52,16 @@ def __init__(self, device_name: str = None, config: Dict = None, *args, **kwargs self.caster = Caster(device_name, config=config) self.remote_screen = None self.playlist_screen = None + self.device_screen = None super().__init__(*args, **kwargs) def on_mount(self): self.remote_screen = RemoteScreen() self.playlist_screen = PlaylistScreen() + self.device_screen = DeviceScreen() self.install_screen(self.remote_screen, name='remote') self.install_screen(self.playlist_screen, name='playlist') + self.install_screen(self.device_screen, name='device') self.push_screen('remote') diff --git a/lolcatt/casting/caster.py b/lolcatt/casting/caster.py index 64499ed..d0c7486 100644 --- a/lolcatt/casting/caster.py +++ b/lolcatt/casting/caster.py @@ -51,6 +51,7 @@ def __init__( self._queue = [] self._current_item = None self._played_queue = [] + self._well_known_devices = {} self._available_devices = None self._catt_call = None self._catt_config = get_config_as_dict() @@ -61,6 +62,7 @@ def __init__( self._update_interval = update_interval self._autoplay = autoplay self._state_last_updated = time.time() + self._init_well_known_devices() self.change_device(name_or_alias) def _build_cast_args(self) -> List[str]: @@ -78,6 +80,13 @@ def _build_cast_args(self) -> List[str]: return catt_cast_args + def _init_well_known_devices(self): + for _, device_name in self._catt_config['aliases'].items(): + try: + self._well_known_devices[device_name] = CattDevice(device_name) + except Exception: + pass + def cast(self, url_or_path: str): """ Casts the given url or path to the currently active device. @@ -210,7 +219,15 @@ def get_played_queue(self) -> List[str]: """ return self._played_queue - def get_available_devices(self) -> List[str]: + def get_well_known_devices(self) -> Dict[str, CattDevice]: + """ + Returns well known devices which have aliases set. + + :return: Well known devices. + """ + return self._well_known_devices + + def get_available_devices(self) -> List[CattDevice]: """ Runs Chromecast discovery and returns a list of available CattDevices. @@ -219,16 +236,13 @@ def get_available_devices(self) -> List[str]: self._available_devices = discover() return self._available_devices - def change_device(self, name_or_alias: str = None): + def _get_device_name(self, name_or_alias: Optional[str]) -> Optional[str]: """ - Changes the currently active device to the given name or alias. If the device is not - available, a ValueError is raised. - - :param name_or_alias: The name or alias of the device to change to. + Returns the device name for the given device name or alias. """ if name_or_alias == 'default': - self._device_name = self._catt_config['options'].get('device') - if self._device_name is None: + device_name = self._catt_config['options'].get('device') + if device_name is None: print( 'No default device set. ' 'Scanning for all available devices and picking first...' @@ -239,25 +253,38 @@ def change_device(self, name_or_alias: str = None): ) possible_devices = self.get_available_devices() if len(possible_devices) > 0: - self._device_name = possible_devices[0].name + device_name = possible_devices[0].name elif name_or_alias == 'None': - self._device_name = None + device_name = None elif name_or_alias is not None: - self._device_name = self._catt_config['aliases'].get(name_or_alias, name_or_alias) + device_name = self._catt_config['aliases'].get(name_or_alias, name_or_alias) else: - self._device_name = None + device_name = None + return device_name - if self._device_name is not None: - try: - self._device = CattDevice(self._device_name) - self._loading_started = None - self._is_loading_cast = False - except CastError: - print(f'Selected device "{self._device_name}" was not found on this network.') - print('Scan for available devices using "lolcatt --scan".') - raise ValueError(f'No device with name or alias "{name_or_alias}" found.') + def change_device(self, name_or_alias: Optional[str] = None): + """ + Changes the currently active device to the given name or alias. If the device is not + available, a ValueError is raised. - def get_device(self) -> CattDevice: + :param name_or_alias: The name or alias of the device to change to. + """ + self._device_name = self._get_device_name(name_or_alias) + + if self._device_name is not None: + if self._device_name in self._well_known_devices: + self._device = self._well_known_devices[self._device_name] + else: + try: + self._device = CattDevice(self._device_name) + except CastError as e: + print(f'Selected device "{self._device_name}" was not found on this network.') + print('Scan for available devices using "lolcatt --scan".') + raise ValueError(f'No device with name or alias "{name_or_alias}" found: {e}') + self._loading_started = None + self._is_loading_cast = False + + def get_device(self) -> Optional[CattDevice]: """ Returns the currently active CattDevice. diff --git a/lolcatt/ui/lolcatt.tcss b/lolcatt/ui/lolcatt.tcss index 1c0b80d..3782869 100644 --- a/lolcatt/ui/lolcatt.tcss +++ b/lolcatt/ui/lolcatt.tcss @@ -119,6 +119,30 @@ LolCatUrlInput { text-style: bold; } +#devicelist_box { + layout: vertical; + align: center middle; + dock: top; + max-height: 18; + max-width: 41; +} + +#devicelist_close_btn { + max-width: 5; + max-height: 3; +} + +#devicelist > ListItem { + padding-top: 1; + padding-bottom: 1; + padding-left: 2; + padding-right: 2; +} + +#active_device_listitem { + text-style: bold; +} + #app { layout: vertical; align: center middle; diff --git a/lolcatt/ui/lolcatt_device_info.py b/lolcatt/ui/lolcatt_device_info.py index 589fae8..e167215 100644 --- a/lolcatt/ui/lolcatt_device_info.py +++ b/lolcatt/ui/lolcatt_device_info.py @@ -38,5 +38,8 @@ def on_mount(self): interval=self.app.caster.get_update_interval(), callback=self._update_label ) + def on_click(self, event): + self.app.push_screen('device') + def compose(self): yield Container(self.label, id='device_info') diff --git a/lolcatt/ui/lolcatt_devicelist.py b/lolcatt/ui/lolcatt_devicelist.py new file mode 100644 index 0000000..dff9f18 --- /dev/null +++ b/lolcatt/ui/lolcatt_devicelist.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +from catt.cli import get_config_as_dict +from textual import on +from textual.containers import Vertical +from textual.reactive import reactive +from textual.widgets import Button +from textual.widgets import Label +from textual.widgets import ListItem +from textual.widgets import ListView +from textual.widgets import Static + + +class LolCattDeviceListView(ListView): + def on_list_view_selected(self, selected): + try: + self.app.caster.change_device(selected.item.devicename) + self.app.pop_screen() + except AttributeError: + pass + + +class LolCattDevicelist(Static): + + devices = reactive([]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.listview = LolCattDeviceListView( + ListItem(Label('Scanning...')), + id="devicelist", + initial_index=None, + ) + self.available_devices = list(get_config_as_dict().get("aliases", {}).values()) + self.active_device = None + self.close_btn = Button("X", id="devicelist_close_btn") + + def compose(self): + yield Vertical(self.listview, self.close_btn, id="devicelist_box") + + def update_list(self): + new_device_name = self.app.caster.get_device_name() + if self.active_device == new_device_name: + return + self.active_device = new_device_name + self.listview.clear() + active_idx = None + for i, device in enumerate(self.available_devices): + if device == new_device_name: + id = "active_device_listitem" + active_idx = i + else: + id = f"device_listitem_{i}" + li_obj = ListItem(Label(device), id=id) + li_obj.devicename = device + self.listview.append(li_obj) + self.listview.index = active_idx + + async def on_mount(self): + self.set_interval(1, self.update_list) + + @on(Button.Pressed, "#devicelist_close_btn") + def on_close_btn(self): + self.app.pop_screen() diff --git a/lolcatt/ui/lolcatt_playlist.py b/lolcatt/ui/lolcatt_playlist.py index 143c563..6665844 100644 --- a/lolcatt/ui/lolcatt_playlist.py +++ b/lolcatt/ui/lolcatt_playlist.py @@ -10,13 +10,17 @@ class LolCattPlaybackListView(ListView): def on_list_view_selected(self, selected): - self.app.caster.cast_at_idx(selected.item.idx) + try: + self.app.caster.cast_at_idx(selected.item.idx) + except AttributeError: + pass class LolCattPlaylist(Static): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.listview = LolCattPlaybackListView( + ListItem(Label('Loading...')), id="playlist", initial_index=None, ) diff --git a/pyproject.toml b/pyproject.toml index 81a2c95..1bfd9e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ exclude = ''' ''' [tool.bumpver] -current_version = "0.4.1" +current_version = "0.5.0" version_pattern = "MAJOR.MINOR.PATCH" commit_message = "bump version {old_version} -> {new_version}" commit = true diff --git a/setup.py b/setup.py index 17039d9..1f28158 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,6 @@ test_suite='tests', tests_require=test_requirements, url='https://github.com/LokiLuciferase/lolcatt', - version='0.4.1', + version='0.5.0', zip_safe=False, )