From df03cd46087445a6ef293f09af85b04690fa4867 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Tue, 20 Feb 2024 02:03:21 +0000 Subject: [PATCH 1/8] Drops the "reboot" feature, since it hasn't been supported in pychromecast in years. There's the possibility of reintroducing this for Sonos, if that's still supported, but the Sonos support is tacked on right now in a way that really hurts the code design, so it'll make more sense to add that back, if needed, than to preserve the functionality in its current form. --- mkchromecast/cast.py | 21 +--------------- mkchromecast/systray.py | 54 ----------------------------------------- 2 files changed, 1 insertion(+), 74 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index f699a116..5240eac8 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -40,7 +40,7 @@ chromecast = False -class Casting(object): +class Casting: """Main casting class.""" def __init__(self, mkcc: mkchromecast.Mkchromecast): @@ -433,25 +433,6 @@ def volume_down(self): self.sonos.volume -= 1 self.sonos.play() - def reboot(self): - try: - from pychromecast.dial import reboot - except ImportError: - # reboot is removed from pychromecast.dial since PR394 - # see: https://github.com/home-assistant-libs/pychromecast/pull/394 - print( - colors.warning( - "This version of pychromecast does not support reboot. Will do nothing." - ) - ) - reboot = lambda x: None - - if self.mkcc.platform == "Darwin": - self.cast.host = socket.gethostbyname(self.cast_to + ".local") - reboot(self.cast.host) - else: - print(colors.error("This method is not supported in Linux yet.")) - # TOOD(xsdg): Unclear how this works, but the self.available_devices method # and the self.available_devices attribute are in obvious conflict. def available_devices(self): diff --git a/mkchromecast/systray.py b/mkchromecast/systray.py index 27978035..9128036f 100644 --- a/mkchromecast/systray.py +++ b/mkchromecast/systray.py @@ -160,7 +160,6 @@ def createUI(self): self.stop_menu() self.volume_menu() self.resetaudio_menu() - self.reboot_menu() self.separator_menu() self.preferences_menu() self.update_menu() @@ -198,10 +197,6 @@ def resetaudio_menu(self): self.ResetAudioAction = self.menu.addAction("Reset Audio") self.ResetAudioAction.triggered.connect(self.reset_audio) - def reboot_menu(self): - self.rebootAction = self.menu.addAction("Reboot Streaming Device") - self.rebootAction.triggered.connect(self.reboot) - def preferences_menu(self): self.preferencesAction = self.menu.addAction("Preferences...") self.preferencesAction.triggered.connect(self.preferences_show) @@ -287,7 +282,6 @@ def cast_list(self): self.stop_menu() self.volume_menu() self.resetaudio_menu() - self.reboot_menu() self.separator_menu() self.preferences_menu() self.update_menu() @@ -359,7 +353,6 @@ def cast_list(self): self.stop_menu() self.volume_menu() self.resetaudio_menu() - self.reboot_menu() self.separator_menu() self.preferences_menu() self.update_menu() @@ -563,53 +556,6 @@ def reset_audio(self): else: remove_sink() - def reboot(self): - try: - from pychromecast.dial import reboot - except ImportError: - # reboot is removed from pychromecast.dial since PR394 - # see: https://github.com/home-assistant-libs/pychromecast/pull/394 - print( - colors.warning( - "This version of pychromecast does not support reboot. Will do nothing." - ) - ) - reboot = lambda x: None - - if _mkcc.platform == "Darwin": - try: - self.cast.host_ = socket.gethostbyname(self.cast_to + ".local") - print("Cast device IP: " + str(self.cast.host_)) - self.reset_audio() - self.stop_cast() - reboot(self.cast.host_) - except socket.gaierror: - print("Cast device IP: " + str(self.cast.host)) - self.reset_audio() - self.stop_cast() - reboot(self.cast.host) - except AttributeError: - # FIXME I should add a notification here - pass - else: - try: - print("Cast device IP: %s" % str(self.cast.host)) - self.reset_audio() - self.stop_cast() - reboot(self.cast.host) - except AttributeError: - self.reset_audio() - self.stop_cast() - try: - for device in self.available_devices: - if self.cast_to in device: - ip = device[3] - print("Sonos device IP: %s" % str(ip)) - url = "http://" + ip + ":1400/reboot" - urlopen(url).read() - except AttributeError: - pass - def preferences_show(self): self.p = mkchromecast.preferences.preferences(self.scale_factor) self.p.show() From 40b3773cf64b73f48ef0bf104c6906d755b4a69a Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Tue, 20 Feb 2024 03:30:24 +0000 Subject: [PATCH 2/8] Half-hearted refactor to separate the Sonos and Chromecast code that was patched together in a way that really hurts code readability, modularity, correctness, etc. This intentionally leaves the Sonos code disabled, so that we can hopefully refactor the Chromecast code into a better design. Then we can re-incorporate Sonos support into that design in a better way. --- mkchromecast/cast.py | 572 ++++++++++++++++++++++++++++++------------- 1 file changed, 406 insertions(+), 166 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 5240eac8..2a6bfb52 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -22,6 +22,7 @@ """ We verify that soco is installed to give Sonos support """ +sonos: bool try: import soco @@ -32,6 +33,7 @@ """ We verify that pychromecast is installed """ +chromecast: bool try: import pychromecast @@ -51,7 +53,6 @@ def __init__(self, mkcc: mkchromecast.Mkchromecast): self.ip = utils.get_effective_ip(self.mkcc.platform, host_override=self.mkcc.host) self.cast: Optional[Any] = None - self.sonos: Optional[Any] = None def _get_chromecasts(self): # TODO(xsdg): Drop backwards compatibility with old versions of @@ -89,26 +90,10 @@ def initialize_cast(self): # This fixes the `No handlers could be found for logger # "pychromecast.socket_client` warning"`. # See commit 18005ebd4c96faccd69757bf3d126eb145687e0d. - if chromecast: - from pychromecast import socket_client - - self.cclist = self._get_chromecasts() - self.cclist = [[i, _, "Gcast"] for i, _ in enumerate(self.cclist)] - else: - self.cclist = [] - - if sonos: - try: - # Checking groups - zone = soco.discovery.any_soco() + from pychromecast import socket_client - self.sonos_list = zone.all_groups - - for self.index, group in enumerate(self.sonos_list): - add_sonos = [self.index, group.coordinator, "Sonos"] - self.cclist.append(add_sonos) - except (TypeError, AttributeError): - pass + self.cclist = self._get_chromecasts() + self.cclist = [[i, _, "Gcast"] for i, _ in enumerate(self.cclist)] if self.mkcc.debug is True: print("self.cclist", self.cclist) @@ -128,10 +113,7 @@ def initialize_cast(self): print(colors.important("Select devices by using the -s flag.")) print(" ") self.cast_to = self.cclist[0][1] - if self.cclist[0][2] == "Sonos": - print(colors.success(self.cast_to.player_name)) - else: - print(colors.success(self.cast_to)) + print(colors.success(self.cast_to)) print(" ") elif ( @@ -242,53 +224,53 @@ def input_device(self, write_to_pickle=True): def get_devices(self): if self.mkcc.debug is True: print("def get_devices(self):") - if chromecast: - try: - if self.mkcc.device_name is not None: - self.cast_to = self.mkcc.device_name - self.cast = self._get_chromecast(self.cast_to) - # Wait for cast device to be ready - self.cast.wait() - print(" ") - print( - colors.important("Information about ") - + " " - + colors.success(self.cast_to) - ) - print(" ") - print(self.cast.device) - print(" ") - print( - colors.important("Status of device ") - + " " - + colors.success(self.cast_to) - ) - print(" ") - print(self.cast.status) - print(" ") - except pychromecast.error.NoChromecastFoundError: - print( - colors.error( - "No Chromecasts matching filter criteria" " were found!" - ) + + try: + if self.mkcc.device_name is not None: + self.cast_to = self.mkcc.device_name + self.cast = self._get_chromecast(self.cast_to) + # Wait for cast device to be ready + self.cast.wait() + print(" ") + print( + colors.important("Information about ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.device) + print(" ") + print( + colors.important("Status of device ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.status) + print(" ") + except pychromecast.error.NoChromecastFoundError: + print( + colors.error( + "No Chromecasts matching filter criteria" " were found!" ) - if self.mkcc.platform == "Darwin": - inputint() - outputint() - elif self.mkcc.platform == "Linux": - remove_sink() - # In the case that the tray is used, we don't kill the - # application - if self.mkcc.operation != OpMode.TRAY: - print(colors.error("Finishing the application...")) - terminate() - exit() - else: - self.stop_cast() - except AttributeError: - pass - except KeyError: - pass + ) + if self.mkcc.platform == "Darwin": + inputint() + outputint() + elif self.mkcc.platform == "Linux": + remove_sink() + # In the case that the tray is used, we don't kill the + # application + if self.mkcc.operation != OpMode.TRAY: + print(colors.error("Finishing the application...")) + terminate() + exit() + else: + self.stop_cast() + except AttributeError: + pass + except KeyError: + pass def play_cast(self): if self.mkcc.debug is True: @@ -311,85 +293,60 @@ def play_cast(self): + " " + self.cast_to.ip_address ) - except AttributeError: # what AttributeError is being expected? - for _ in self.sonos_list: - if self.cast_to == _.player_name: - self.cast_to = _ - print( - colors.options("The IP of ") - + colors.success(self.cast_to.player_name) - + colors.options(" is:") - + " " - + self.cast_to.ip_address - ) if self.mkcc.host is None: print(colors.options("Your local IP is:") + " " + localip) else: print(colors.options("Your manually entered local IP is:") + " " + localip) - try: - media_controller = self.cast.media_controller - - # Set up the mime type and conditionally import video or audio - # TODO(xsdg): Get rid of these conditional imports. - media_type: str - if self.mkcc.videoarg: - import mkchromecast.video - - # TODO(xsdg): Centralize media type storage in some way. - # In mkcc? - media_type = self.mkcc.mtype or "video/mp4" - else: - import mkchromecast.audio + media_controller = self.cast.media_controller - media_type = mkchromecast.audio.media_type - print(" ") - print(colors.options("Using media type:") + f" {media_type}") + # Set up the mime type and conditionally import video or audio + # TODO(xsdg): Get rid of these conditional imports. + media_type: str + if self.mkcc.videoarg: + import mkchromecast.video - play_url: str - if self.mkcc.operation == OpMode.SOURCE_URL: - play_url = self.mkcc.source_url - print(colors.options("Casting from stream URL:") - + f" {play_url}") - else: - play_url = f"http://{localip}:{self.mkcc.port}/stream" + # TODO(xsdg): Centralize media type storage in some way. + # In mkcc? + media_type = self.mkcc.mtype or "video/mp4" + else: + import mkchromecast.audio - media_controller.play_media( - play_url, media_type, title=self.title, stream_type="LIVE", - ) + media_type = mkchromecast.audio.media_type + print(" ") + print(colors.options("Using media type:") + f" {media_type}") - if media_controller.is_active: - media_controller.play() + play_url: str + if self.mkcc.operation == OpMode.SOURCE_URL: + play_url = self.mkcc.source_url + print(colors.options("Casting from stream URL:") + + f" {play_url}") + else: + play_url = f"http://{localip}:{self.mkcc.port}/stream" - print(" ") - print(colors.important("Cast media controller status")) - print(" ") - print(self.cast.status) - print(" ") + media_controller.play_media( + play_url, media_type, title=self.title, stream_type="LIVE", + ) - time.sleep(5.0) + if media_controller.is_active: media_controller.play() - if self.mkcc.hijack is True: - self.r = Thread(target=self.hijack_cc) - # This has to be set to True so that we catch - # KeyboardInterrupt. - self.r.daemon = True - self.r.start() + print(" ") + print(colors.important("Cast media controller status")) + print(" ") + print(self.cast.status) + print(" ") - # TODO(xsdg): This isn't an appropriate exception-handling strategy. - except AttributeError: - raise Exception("Internal error: This code path is broken and " - "needs to be fixed.") - self.sonos = self.cast_to - self.sonos.play_uri( - "x-rincon-mp3radio://" + localip + ":" + self.mkcc.port + "/stream", - title=self.title, - ) - if self.mkcc.operation == OpMode.TRAY: - # TODO(xsdg): No. - self.cast = self.sonos + time.sleep(5.0) + media_controller.play() + + if self.mkcc.hijack is True: + self.r = Thread(target=self.hijack_cc) + # This has to be set to True so that we catch + # KeyboardInterrupt. + self.r.daemon = True + self.r.start() def pause(self): """Pause casting""" @@ -404,8 +361,6 @@ def play(self): def stop_cast(self): if self.cast: self.cast.quit_app() - if self.sonos: - self.sonos.stop() def volume_up(self): """Increment volume by 0.1 unless it is already maxed. @@ -413,12 +368,8 @@ def volume_up(self): """ if self.mkcc.debug is True: print("Increasing volume... \n") - try: - volume = round(self.cast.status.volume_level, 1) - return self.cast.set_volume(volume + 0.1) - except AttributeError: - self.sonos.volume += 1 - self.sonos.play() + volume = round(self.cast.status.volume_level, 1) + return self.cast.set_volume(volume + 0.1) def volume_down(self): """Decrement the volume by 0.1 unless it is already 0. @@ -426,12 +377,8 @@ def volume_down(self): """ if self.mkcc.debug is True: print("Decreasing volume... \n") - try: - volume = round(self.cast.status.volume_level, 1) - return self.cast.set_volume(volume - 0.1) - except AttributeError: - self.sonos.volume -= 1 - self.sonos.play() + volume = round(self.cast.status.volume_level, 1) + return self.cast.set_volume(volume - 0.1) # TOOD(xsdg): Unclear how this works, but the self.available_devices method # and the self.available_devices attribute are in obvious conflict. @@ -441,24 +388,10 @@ def available_devices(self): """ self.available_devices = [] for self.index, device in enumerate(self.cclist): - try: - types = device[2] - if types == "Sonos": - device_ip = device[1].ip_address - device = device[1].player_name - else: - device = device[1] - except UnicodeEncodeError: - types = device[2] - if types == "Sonos": - device_ip = device[1].ip_address - device = device[1].player_name - else: - device = device[1] - if types == "Sonos": - to_append = [self.index, device, types, device_ip] - else: - to_append = [self.index, device, types] + types = device[2] + device = device[1] + + to_append = [self.index, device, types] self.available_devices.append(to_append) return self.available_devices @@ -518,3 +451,310 @@ def ping_chromecast(ip): except: return False return True + + +class _DisabledSonosCasting: + """Half-hearted attempt at refactoring Sonos support into its own class. + + This is broken, but should simplify the Chromecast support code until the + Sonos support can be unbroken at some later point. + """ + + def __init__(self, mkcc: mkchromecast.Mkchromecast): + self.mkcc = mkcc + + self.title = "Mkchromecast v" + __version__ + + self.ip = utils.get_effective_ip(self.mkcc.platform, host_override=self.mkcc.host) + + self.sonos: Optional[Any] = None + + """ + Cast processes + """ + + def initialize_cast(self): + self.cclist: list[Any] = [] + if sonos: + # Checking groups + zone = soco.discovery.any_soco() + + self.sonos_list = zone.all_groups + + for self.index, group in enumerate(self.sonos_list): + add_sonos = [self.index, group.coordinator, "Sonos"] + self.cclist.append(add_sonos) + + if self.mkcc.debug is True: + print("self.cclist", self.cclist) + + if ( + len(self.cclist) != 0 + and self.mkcc.select_device is False + and self.mkcc.device_name is None + ): + if self.mkcc.debug is True: + print("if len(self.cclist) != 0 and self.mkcc.select_device == False:") + print(" ") + print_available_devices(self.available_devices()) + print(" ") + if self.mkcc.operation != OpMode.DISCOVER: + print(colors.important("Casting to first device shown above!")) + print(colors.important("Select devices by using the -s flag.")) + print(" ") + self.cast_to = self.cclist[0][1] + if self.cclist[0][2] == "Sonos": + print(colors.success(self.cast_to.player_name)) + else: + print(colors.success(self.cast_to)) + print(" ") + + elif ( + len(self.cclist) != 0 + and self.mkcc.select_device is True + and self.mkcc.operation != OpMode.TRAY + and self.mkcc.device_name is None + ): + if self.mkcc.debug is True: + print( + "elif len(self.cclist) != 0 and self.mkcc.select_device == True" + " and self.mkcc.tray == False:" + ) + if os.path.exists("/tmp/mkchromecast.tmp") is False: + self.tf = open("/tmp/mkchromecast.tmp", "wb") + print(" ") + print_available_devices(self.available_devices()) + else: + if self.mkcc.debug is True: + print("else:") + self.tf = open("/tmp/mkchromecast.tmp", "rb") + self.index = pickle.load(self.tf) + self.cast_to = self.cclist[int(self.index)] + print(" ") + print( + colors.options("Casting to:") + " " + colors.success(self.cast_to) + ) + print(" ") + + elif len(self.cclist) != 0 and self.mkcc.select_device and self.mkcc.operation == OpMode.TRAY: + if self.mkcc.debug is True: + print( + "elif len(self.cclist) != 0 and self.mkcc.select_device == True" + " and self.mkcc.tray == True:" + ) + if os.path.exists("/tmp/mkchromecast.tmp") is False: + self.tf = open("/tmp/mkchromecast.tmp", "wb") + print(" ") + print_available_devices(self.available_devices()) + else: + if self.mkcc.debug is True: + print("else:") + self.tf = open("/tmp/mkchromecast.tmp", "rb") + self.cast_to = pickle.load(self.tf) + print_available_devices(self.available_devices()) + print(" ") + print( + colors.options("Casting to:") + " " + colors.success(self.cast_to) + ) + print(" ") + + elif len(self.cclist) == 0 and self.mkcc.operation != OpMode.TRAY: + if self.mkcc.debug is True: + print("elif len(self.cclist) == 0 and self.mkcc.tray == False:") + print(colors.error("No devices found!")) + if self.mkcc.platform == "Linux" and self.mkcc.adevice is None: + remove_sink() + elif self.mkcc.platform == "Darwin": + inputint() + outputint() + terminate() + exit() + + elif len(self.cclist) == 0 and self.mkcc.operation == OpMode.TRAY: + print(colors.error(":::Tray::: No devices found!")) + self.available_devices = [] + + def select_a_device(self): + print(" ") + print( + "Please, select the " + + colors.important("Index") + + " of the Google Cast device that you want to use:" + ) + self.index = input() + + def input_device(self, write_to_pickle=True): + while True: + try: + if write_to_pickle: + pickle.dump(self.index, self.tf) + self.tf.close() + self.cast_to = self.cclist[int(self.index)][1] + print(" ") + print( + colors.options("Casting to:") + " " + colors.success(self.cast_to) + ) + print(" ") + except TypeError: + print( + colors.options("Casting to:") + + " " + + colors.success(self.cast_to.player_name) + ) + except IndexError: + checkmktmp() + self.tf = open("/tmp/mkchromecast.tmp", "wb") + # TODO(xsdg): The original code had what was likely a typo here, + # in that this called `self.select_device()`, which did not + # exist. It likely was supposed to be `self.select_a_device()`, + # but it's better to just start over, here. + raise Exception( + "Internal error: Never worked; needs to be fixed.") + self.mkcc.select_device() + continue + break + + def get_devices(self): + if self.mkcc.debug is True: + print("def get_devices(self):") + if chromecast: + try: + if self.mkcc.device_name is not None: + self.cast_to = self.mkcc.device_name + self.cast = self._get_chromecast(self.cast_to) + # Wait for cast device to be ready + self.cast.wait() + print(" ") + print( + colors.important("Information about ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.device) + print(" ") + print( + colors.important("Status of device ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.status) + print(" ") + except pychromecast.error.NoChromecastFoundError: + print( + colors.error( + "No Chromecasts matching filter criteria" " were found!" + ) + ) + if self.mkcc.platform == "Darwin": + inputint() + outputint() + elif self.mkcc.platform == "Linux": + remove_sink() + # In the case that the tray is used, we don't kill the + # application + if self.mkcc.operation != OpMode.TRAY: + print(colors.error("Finishing the application...")) + terminate() + exit() + else: + self.stop_cast() + except AttributeError: + pass + except KeyError: + pass + + def play_cast(self): + if self.mkcc.debug is True: + print("def play_cast(self):") + localip = self.ip + + for sonos_target in self.sonos_list: + if self.cast_to == sonos_target.player_name: + self.cast_to = sonos_target + print( + colors.options("The IP of ") + + colors.success(self.cast_to.player_name) + + colors.options(" is:") + + " " + + self.cast_to.ip_address + ) + + if self.mkcc.host is None: + print(colors.options("Your local IP is:") + " " + localip) + else: + print(colors.options("Your manually entered local IP is:") + " " + localip) + + raise Exception("Internal error: This code path is broken and " + "needs to be fixed.") + self.sonos = self.cast_to + self.sonos.play_uri( + "x-rincon-mp3radio://" + localip + ":" + self.mkcc.port + "/stream", + title=self.title, + ) + if self.mkcc.operation == OpMode.TRAY: + # TODO(xsdg): No. + self.cast = self.sonos + + def pause(self): + """Pause casting""" + media_controller = self.cast.media_controller + media_controller.pause() + + def play(self): + """Play casting""" + media_controller = self.cast.media_controller + media_controller.play() + + def stop_cast(self): + if self.sonos: + self.sonos.stop() + + def volume_up(self): + """Increment volume by 0.1 unless it is already maxed. + Returns the new volume. + """ + if self.mkcc.debug is True: + print("Increasing volume... \n") + self.sonos.volume += 1 + self.sonos.play() + + def volume_down(self): + """Decrement the volume by 0.1 unless it is already 0. + Returns the new volume. + """ + if self.mkcc.debug is True: + print("Decreasing volume... \n") + self.sonos.volume -= 1 + self.sonos.play() + + # TOOD(xsdg): Unclear how this works, but the self.available_devices method + # and the self.available_devices attribute are in obvious conflict. + def available_devices(self): + """This method is uplay_mediased for populating the self.available_devices array + needed for the system tray. + """ + self.available_devices = [] + for self.index, device in enumerate(self.cclist): + try: + types = device[2] + if types == "Sonos": + device_ip = device[1].ip_address + device = device[1].player_name + else: + device = device[1] + except UnicodeEncodeError: + types = device[2] + if types == "Sonos": + device_ip = device[1].ip_address + device = device[1].player_name + else: + device = device[1] + if types == "Sonos": + to_append = [self.index, device, types, device_ip] + else: + to_append = [self.index, device, types] + self.available_devices.append(to_append) + + return self.available_devices From 7ab8513e8a32d15f46af0cc3b48be6b9c1647fc0 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Tue, 20 Feb 2024 03:38:49 +0000 Subject: [PATCH 3/8] Renames "sonos" and "chromecast" to "has_sonos" and "has_chromecast" to clarify their meanings. --- mkchromecast/cast.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 2a6bfb52..6f78628b 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -22,24 +22,24 @@ """ We verify that soco is installed to give Sonos support """ -sonos: bool +has_sonos: bool try: import soco - sonos = True + has_sonos = True except ImportError: - sonos = False + has_sonos = False """ We verify that pychromecast is installed """ -chromecast: bool +has_chromecast: bool try: import pychromecast - chromecast = True + has_chromecast = True except ImportError: - chromecast = False + has_chromecast = False class Casting: @@ -475,7 +475,7 @@ def __init__(self, mkcc: mkchromecast.Mkchromecast): def initialize_cast(self): self.cclist: list[Any] = [] - if sonos: + if has_sonos: # Checking groups zone = soco.discovery.any_soco() @@ -617,7 +617,7 @@ def input_device(self, write_to_pickle=True): def get_devices(self): if self.mkcc.debug is True: print("def get_devices(self):") - if chromecast: + if has_chromecast: try: if self.mkcc.device_name is not None: self.cast_to = self.mkcc.device_name From f2443e0906e6ed4ed5e1b0185ca42a581ad98aff Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Fri, 8 Mar 2024 07:42:48 +0000 Subject: [PATCH 4/8] Creates an AvailableDevice dataclass, resolves the available_devices method/attribute ambiguity, and moves print_available_devices to the cast module. --- bin/mkchromecast | 9 ++--- mkchromecast/cast.py | 87 +++++++++++++++++++++++++--------------- mkchromecast/messages.py | 13 ------ 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/bin/mkchromecast b/bin/mkchromecast index eca56583..12aabf45 100755 --- a/bin/mkchromecast +++ b/bin/mkchromecast @@ -16,12 +16,11 @@ import mkchromecast from mkchromecast.version import __version__ from mkchromecast.audio_devices import (inputint, inputdev, outputdev, outputint) -from mkchromecast.cast import Casting -import mkchromecast.colors as colors +from mkchromecast import cast +from mkchromecast import colors from mkchromecast.constants import OpMode from mkchromecast.pulseaudio import create_sink, get_sink_list, remove_sink from mkchromecast.utils import terminate, checkmktmp, writePidFile -from mkchromecast.messages import print_available_devices def maybe_execute_single_action(mkcc: mkchromecast.Mkchromecast): @@ -51,10 +50,10 @@ class CastProcess(object): self.mkcc = mkcc # Type declarations - self.cc: Casting + self.cc: cast.Casting def run(self): - self.cc = Casting(self.mkcc) + self.cc = cast.Casting(self.mkcc) checkmktmp() writePidFile() diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 6f78628b..799b678b 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -1,12 +1,13 @@ # This file is part of mkchromecast. +import dataclasses import os import pickle import socket import subprocess from threading import Thread import time -from typing import Any, Optional +from typing import Any, Iterable, Optional import mkchromecast from mkchromecast import colors @@ -15,7 +16,6 @@ from mkchromecast.constants import OpMode from mkchromecast.utils import terminate, checkmktmp from mkchromecast.pulseaudio import remove_sink -from mkchromecast.messages import print_available_devices from mkchromecast.version import __version__ @@ -42,6 +42,26 @@ has_chromecast = False +@dataclasses.dataclass +class AvailableDevice: + index: int + name: pychromecast.Chromecast + type: str + + def __str__(self): + return f"{self.index} \t{self.type} \t{self.name}" + + +def print_available_devices(devices: Iterable[AvailableDevice]): + """Prints a list of available devices.""" + print(colors.important("List of Devices Available in Network:")) + print(colors.important("-------------------------------------\n")) + print(colors.important("Index Type Friendly Name ")) + print(colors.important("===== ===== ============= ")) + for device in devices: + print(device) + + class Casting: """Main casting class.""" @@ -52,18 +72,10 @@ def __init__(self, mkcc: mkchromecast.Mkchromecast): self.ip = utils.get_effective_ip(self.mkcc.platform, host_override=self.mkcc.host) - self.cast: Optional[Any] = None + self.cast: Optional[pychromecast.Chromecast] = None + self._chromecasts_by_name: dict[str, pychromecast.Chromecast] def _get_chromecasts(self): - # TODO(xsdg): Drop backwards compatibility with old versions of - # pychromecast - - # compatibility - try: - return list(pychromecast.get_chromecasts_as_dict().keys()) - except AttributeError: - pass - _chromecasts = pychromecast.get_chromecasts(tries=self.mkcc.tries) # since PR380, pychromecast.get_chromecasts returns a tuple @@ -78,8 +90,10 @@ def _get_chromecasts(self): def _get_chromecast(self, name): # compatibility try: + print("gc1") return pychromecast.get_chromecast(friendly_name=self.cast_to) except AttributeError: + print("gc2") return self._chromecasts_by_name[name] """ @@ -92,8 +106,8 @@ def initialize_cast(self): # See commit 18005ebd4c96faccd69757bf3d126eb145687e0d. from pychromecast import socket_client - self.cclist = self._get_chromecasts() - self.cclist = [[i, _, "Gcast"] for i, _ in enumerate(self.cclist)] + tmp_cclist = self._get_chromecasts() + self.cclist = [[i, name, "Gcast"] for i, name in enumerate(tmp_cclist)] if self.mkcc.debug is True: print("self.cclist", self.cclist) @@ -106,7 +120,7 @@ def initialize_cast(self): if self.mkcc.debug is True: print("if len(self.cclist) != 0 and self.mkcc.select_device == False:") print(" ") - print_available_devices(self.available_devices()) + print_available_devices(self.available_devices) print(" ") if self.mkcc.operation != OpMode.DISCOVER: print(colors.important("Casting to first device shown above!")) @@ -130,7 +144,7 @@ def initialize_cast(self): if os.path.exists("/tmp/mkchromecast.tmp") is False: self.tf = open("/tmp/mkchromecast.tmp", "wb") print(" ") - print_available_devices(self.available_devices()) + print_available_devices(self.available_devices) else: if self.mkcc.debug is True: print("else:") @@ -152,13 +166,13 @@ def initialize_cast(self): if os.path.exists("/tmp/mkchromecast.tmp") is False: self.tf = open("/tmp/mkchromecast.tmp", "wb") print(" ") - print_available_devices(self.available_devices()) + print_available_devices(self.available_devices) else: if self.mkcc.debug is True: print("else:") self.tf = open("/tmp/mkchromecast.tmp", "rb") self.cast_to = pickle.load(self.tf) - print_available_devices(self.available_devices()) + print_available_devices(self.available_devices) print(" ") print( colors.options("Casting to:") + " " + colors.success(self.cast_to) @@ -179,7 +193,6 @@ def initialize_cast(self): elif len(self.cclist) == 0 and self.mkcc.operation == OpMode.TRAY: print(colors.error(":::Tray::: No devices found!")) - self.available_devices = [] def select_a_device(self): print(" ") @@ -275,6 +288,11 @@ def get_devices(self): def play_cast(self): if self.mkcc.debug is True: print("def play_cast(self):") + if not self.cast: + print(colors.warning("Calling get_devices before proceeding with play_cast")) + self.get_devices() + if not self.cast: + raise Exception("Internal error, self.cast was not set.") localip = self.ip try: @@ -350,11 +368,15 @@ def play_cast(self): def pause(self): """Pause casting""" + if not self.cast: + raise Exception("Internal error: not initialized.") media_controller = self.cast.media_controller media_controller.pause() def play(self): """Play casting""" + if not self.cast: + raise Exception("Internal error: not initialized.") media_controller = self.cast.media_controller media_controller.play() @@ -366,6 +388,8 @@ def volume_up(self): """Increment volume by 0.1 unless it is already maxed. Returns the new volume. """ + if not self.cast: + raise Exception("Internal error: not initialized.") if self.mkcc.debug is True: print("Increasing volume... \n") volume = round(self.cast.status.volume_level, 1) @@ -375,26 +399,21 @@ def volume_down(self): """Decrement the volume by 0.1 unless it is already 0. Returns the new volume. """ + if not self.cast: + raise Exception("Internal error: not initialized.") if self.mkcc.debug is True: print("Decreasing volume... \n") volume = round(self.cast.status.volume_level, 1) return self.cast.set_volume(volume - 0.1) - # TOOD(xsdg): Unclear how this works, but the self.available_devices method - # and the self.available_devices attribute are in obvious conflict. - def available_devices(self): - """This method is uplay_mediased for populating the self.available_devices array - needed for the system tray. - """ - self.available_devices = [] - for self.index, device in enumerate(self.cclist): - types = device[2] - device = device[1] + @property + def available_devices(self) -> list[AvailableDevice]: + """The list of available devices.""" + devices: list[AvailableDevice] = [] + for device_index, (_, name, type_) in enumerate(self.cclist): + devices.append(AvailableDevice(device_index, name, type_)) - to_append = [self.index, device, types] - self.available_devices.append(to_append) - - return self.available_devices + return devices def hijack_cc(self): """Dummy method to call _hijack_cc_(). @@ -424,6 +443,8 @@ def _hijack_cc_(self): name is different from "Default Media Receiver", it hijacks to the chromecast. """ + if not self.cast: + raise Exception("Internal error: not initialized.") ip = self.cast.socket_client.host # valid since at least v3.0.0 diff --git a/mkchromecast/messages.py b/mkchromecast/messages.py index 8460fd60..7f29525a 100644 --- a/mkchromecast/messages.py +++ b/mkchromecast/messages.py @@ -16,16 +16,3 @@ def print_samplerate_warning(codec: str) -> None: f"Sample rates supported by {codec} are: {joined_rates}." ) ) - - -def print_available_devices(list_of_devices: Iterable[Any]): - """Prints a list of available devices.""" - print(colors.important("List of Devices Available in Network:")) - print(colors.important("-------------------------------------\n")) - print(colors.important("Index Types Friendly Name ")) - print(colors.important("===== ===== ============= ")) - for device in list_of_devices: - device_index = device[0] - device_name = device[1] - device_type = device[2] - print("%s \t%s \t%s" % (device_index, device_type, device_name)) From ded93ea2a0ffc41ff3fe88a839ce697346faf819 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Fri, 8 Mar 2024 08:26:04 +0000 Subject: [PATCH 5/8] Updates the systray infrastructure to adopt the new AvailableDevice patterns. --- mkchromecast/systray.py | 47 +++++++++++++++++----------------- mkchromecast/tray_threading.py | 31 ++++++++-------------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/mkchromecast/systray.py b/mkchromecast/systray.py index 9128036f..ea53cda0 100644 --- a/mkchromecast/systray.py +++ b/mkchromecast/systray.py @@ -11,12 +11,12 @@ from urllib.request import urlopen import mkchromecast +from mkchromecast import cast from mkchromecast import colors from mkchromecast import config from mkchromecast import preferences from mkchromecast import tray_threading from mkchromecast.audio_devices import inputint, outputint -from mkchromecast.cast import Casting from mkchromecast.pulseaudio import remove_sink from mkchromecast.utils import del_tmp, checkmktmp from mkchromecast.version import __version__ @@ -42,12 +42,15 @@ class menubar(QtWidgets.QMainWindow): def __init__(self): - self.cc = Casting(_mkcc) + self.cc = cast.Casting(_mkcc) signal.signal(signal.SIGINT, signal.SIG_DFL) self.cast = None self.stopped = False self.played = False self.pcastfailed = False + + self.available_devices: list[cast.AvailableDevice] = [] + # TODO(xsdg): pull this directly from _mkcc. self.config = config.Config(platform=_mkcc.platform, read_only=True, @@ -217,7 +220,7 @@ def exit_menu(self): These are methods for interacting with the mkchromecast objects """ - def onIntReady(self, available_devices): + def onIntReady(self, available_devices: list): print("available_devices received") self.available_devices = available_devices self.cast_list() @@ -271,7 +274,7 @@ def search_cast(self): def cast_list(self): self.set_icon_idle() - if len(self.available_devices) == 0: + if not self.available_devices: self.menu.clear() self.search_menu() self.separator_menu() @@ -328,16 +331,13 @@ def cast_list(self): self.search_menu() self.separator_menu() print("Available Media Streaming Devices", self.available_devices) - for index, menuentry in enumerate(self.available_devices): - try: - a = self.ag.addAction( - (QtWidgets.QAction(str(menuentry[1]), self, checkable=True)) - ) - self.menuentry = self.menu.addAction(a) - except UnicodeEncodeError: - a = self.menuentry = self.menu.addAction( - str(unicode(menuentry[1]).encode("utf-8")) - ) + for index, device in enumerate(self.available_devices): + # TODO(xsdg): self.ag isn't actually referenced from anywhere, + # so just make it local. + action = self.ag.addAction( + (QtWidgets.QAction(device.name, self, checkable=True)) + ) + # The receiver is a lambda function that passes clicked as # a boolean, and the clicked_item as an argument to the # self.clicked_cc() method. This last method, sets the correct @@ -345,10 +345,12 @@ def cast_list(self): # self.play_cast(). Credits to this question in stackoverflow: # # http://stackoverflow.com/questions/1464548/pyqt-qmenu-dynamically-populated-and-clicked - receiver = lambda clicked, clicked_item=menuentry: self.clicked_cc( + receiver = lambda clicked, clicked_item=device: self.clicked_cc( clicked_item ) - a.triggered.connect(receiver) + action.triggered.connect(receiver) + + self.menu.addAction(action) self.separator_menu() self.stop_menu() self.volume_menu() @@ -359,17 +361,14 @@ def cast_list(self): self.about_menu() self.exit_menu() - def clicked_cc(self, clicked_item): - if self.played is True: - try: - self.cast.quit_app() - except AttributeError: - self.cast.stop() + def clicked_cc(self, clicked_item: cast.AvailableDevice): + if self.played: + self.cast.quit_app() if _mkcc.debug is True: print(":::tray::: clicked item: %s." % clicked_item) - self.index = clicked_item[0] - self.cast_to = clicked_item[1] + self.index = clicked_item.index + self.cast_to = clicked_item.name self.play_cast() def pcastready(self, message): diff --git a/mkchromecast/tray_threading.py b/mkchromecast/tray_threading.py index 951e559b..a8134f2b 100644 --- a/mkchromecast/tray_threading.py +++ b/mkchromecast/tray_threading.py @@ -5,11 +5,11 @@ import mkchromecast from mkchromecast import audio +from mkchromecast import cast from mkchromecast import colors from mkchromecast import config from mkchromecast import node from mkchromecast.audio_devices import inputdev, outputdev -from mkchromecast.cast import Casting from mkchromecast.constants import OpMode from mkchromecast.pulseaudio import create_sink, check_sink from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot @@ -28,26 +28,15 @@ def _search_cast_(self): # This should fix the error socket.gaierror making the system tray to # be closed. try: - self.cc = Casting(_mkcc) - self.cc.initialize_cast() - self.cc.available_devices() + cc = cast.Casting(_mkcc) + cc.initialize_cast() + self.intReady.emit(cc.available_devices) + self.finished.emit() except socket.gaierror: if _mkcc.debug is True: - print(colors.warning(":::Threading::: Socket error, CC set to 0")) - pass - except TypeError: - # TODO(xsdg): this is probably a bad idea. - pass - except OSError: - self.cc.available_devices = [] - - if len(self.cc.available_devices) == 0 and _mkcc.operation == OpMode.TRAY: - available_devices = [] - self.intReady.emit(available_devices) - self.finished.emit() - else: - available_devices = self.cc.available_devices - self.intReady.emit(available_devices) + print(colors.warning( + ":::Threading::: Socket error, failed to search for devices")) + self.intReady.emit([]) self.finished.emit() @@ -78,7 +67,7 @@ def _play_cast_(self): if check_sink() is False and _mkcc.adevice is None: create_sink() - start = Casting(_mkcc) + start = cast.Casting(_mkcc) start.initialize_cast() try: start.get_devices() @@ -108,7 +97,7 @@ class Updater(QObject): @pyqtSlot() def _updater_(self): - chk = Casting(_mkcc) + chk = cast.Casting(_mkcc) if chk.ip == "127.0.0.1" or None: # We verify the local IP. self.updateready.emit("None") else: From deebb8df3b36182c248b1ea7e634164ee0936d21 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Fri, 8 Mar 2024 08:56:19 +0000 Subject: [PATCH 6/8] Further cast.py simplifications. --- mkchromecast/cast.py | 83 +++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 55 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 799b678b..3947e2bb 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -87,15 +87,6 @@ def _get_chromecasts(self): return list(self._chromecasts_by_name.keys()) - def _get_chromecast(self, name): - # compatibility - try: - print("gc1") - return pychromecast.get_chromecast(friendly_name=self.cast_to) - except AttributeError: - print("gc2") - return self._chromecasts_by_name[name] - """ Cast processes """ @@ -238,52 +229,34 @@ def get_devices(self): if self.mkcc.debug is True: print("def get_devices(self):") - try: - if self.mkcc.device_name is not None: - self.cast_to = self.mkcc.device_name - self.cast = self._get_chromecast(self.cast_to) - # Wait for cast device to be ready - self.cast.wait() - print(" ") - print( - colors.important("Information about ") - + " " - + colors.success(self.cast_to) - ) - print(" ") - print(self.cast.device) - print(" ") - print( - colors.important("Status of device ") - + " " - + colors.success(self.cast_to) - ) - print(" ") - print(self.cast.status) - print(" ") - except pychromecast.error.NoChromecastFoundError: - print( - colors.error( - "No Chromecasts matching filter criteria" " were found!" - ) - ) - if self.mkcc.platform == "Darwin": - inputint() - outputint() - elif self.mkcc.platform == "Linux": - remove_sink() - # In the case that the tray is used, we don't kill the - # application - if self.mkcc.operation != OpMode.TRAY: - print(colors.error("Finishing the application...")) - terminate() - exit() - else: - self.stop_cast() - except AttributeError: - pass - except KeyError: - pass + if self.mkcc.device_name: + self.cast_to = self.mkcc.device_name + + if self.cast_to not in self._chromecasts_by_name: + self.cast = None + print(colors.warning(f"No chromecast found named {self.cast_to}")) + return + + self.cast = self._chromecasts_by_name[self.cast_to] + # Wait for cast device to be ready + self.cast.wait() + print() + print( + colors.important("Information about ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.device) + print(" ") + print( + colors.important("Status of device ") + + " " + + colors.success(self.cast_to) + ) + print(" ") + print(self.cast.status) + print(" ") def play_cast(self): if self.mkcc.debug is True: From 97c96f74fa13fd9c7de4355a44cc5c15524632b1 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Fri, 8 Mar 2024 09:12:39 +0000 Subject: [PATCH 7/8] Fixes an alternate codepath and performs extra simplification --- mkchromecast/cast.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 3947e2bb..082e7935 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -234,29 +234,35 @@ def get_devices(self): if self.cast_to not in self._chromecasts_by_name: self.cast = None - print(colors.warning(f"No chromecast found named {self.cast_to}")) - return + print(colors.warning(f"No Chromecast found named {self.cast_to}")) + + if self.mkcc.platform == "Darwin": + inputint() + outputint() + elif self.mkcc.platform == "Linux": + remove_sink() + + # In the case that the tray is used, we don't kill the + # application + if self.mkcc.operation == OpMode.TRAY: + return + + print(colors.error("Finishing the application...")) + terminate() + exit() self.cast = self._chromecasts_by_name[self.cast_to] # Wait for cast device to be ready self.cast.wait() print() - print( - colors.important("Information about ") - + " " - + colors.success(self.cast_to) - ) - print(" ") - print(self.cast.device) - print(" ") print( colors.important("Status of device ") + " " + colors.success(self.cast_to) ) - print(" ") + print() print(self.cast.status) - print(" ") + print() def play_cast(self): if self.mkcc.debug is True: From 20023f4442521a1e9e9ec13106550da6b193e5b9 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Fri, 8 Mar 2024 09:21:25 +0000 Subject: [PATCH 8/8] Minor fixups --- mkchromecast/cast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mkchromecast/cast.py b/mkchromecast/cast.py index 082e7935..39e77001 100644 --- a/mkchromecast/cast.py +++ b/mkchromecast/cast.py @@ -45,7 +45,7 @@ @dataclasses.dataclass class AvailableDevice: index: int - name: pychromecast.Chromecast + name: str type: str def __str__(self): @@ -75,7 +75,7 @@ def __init__(self, mkcc: mkchromecast.Mkchromecast): self.cast: Optional[pychromecast.Chromecast] = None self._chromecasts_by_name: dict[str, pychromecast.Chromecast] - def _get_chromecasts(self): + def _get_chromecast_names(self) -> list[str]: _chromecasts = pychromecast.get_chromecasts(tries=self.mkcc.tries) # since PR380, pychromecast.get_chromecasts returns a tuple @@ -97,7 +97,7 @@ def initialize_cast(self): # See commit 18005ebd4c96faccd69757bf3d126eb145687e0d. from pychromecast import socket_client - tmp_cclist = self._get_chromecasts() + tmp_cclist = self._get_chromecast_names() self.cclist = [[i, name, "Gcast"] for i, name in enumerate(tmp_cclist)] if self.mkcc.debug is True: