#!/usr/bin/env python3 import sys import threading import time import os from typing import Callable import pychromecast import wx import wx.lib.scrolledpanel as scrolled import wx.media from vlc import Instance from Channel import SVT from ChannelProvider import ChannelProvider from Utils import (get_all_svt_categories, make_bitmap_from_file, make_sized_button, BASEPATH) WIDTH = int(720 / 2) HEIGHT = int(1440 / 2) BTN_HEIGHT = 40 SPACER_HEIGHT = 10 SCROLL_RATE = 5 BM_BTN_STYLE = wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP BTN_STYLE = wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT FLAGS = wx.SizerFlags() FLAGS.Center() class Cast(wx.Frame): def __init__(self, *args, **kw): """__init__. :param args: :param kw: """ self.m_mode = 'normal' self.asset_path: str = '/usr/share/cast' url = None if "url" in kw: self.m_mode = 'single' url = kw['url'] del kw['url'] super().__init__(*args, **kw) if not os.path.isdir(BASEPATH): os.mkdir(BASEPATH) self.m_selected_chromecast = None self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH) self.m_style = self.GetWindowStyle() self.m_chromecast_thr = threading.Thread(target=self.get_chromecasts, args=(), kwargs={}) self.m_vlc = Instance() self.m_vlc_listplayer = self.m_vlc.media_list_player_new() self.m_chromecast_thr.start() self.m_sizer: wx.Sizer = wx.BoxSizer(wx.VERTICAL) self.m_panel: wx.lib.scrolledpanel.ScrolledPanel = scrolled.ScrolledPanel( # type: ignore self, -1, style=wx.VSCROLL) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_providers: list[ChannelProvider] = self.get_providers() if url: self.show_player(None, url, 0) elif len(self.m_providers) > 1: self.show_provider_list(None) else: self.show_channel_list(None, 0) def add_back_button(self, callback: Callable) -> None: backbtn = wx.Button(self.m_panel, -1, label="Go back", size=(WIDTH, BTN_HEIGHT)) backbtn.Bind(wx.EVT_BUTTON, callback) self.m_sizer.Add(backbtn) def get_chromecasts(self) -> None: """ [TODO:description] :rtype None: [TODO:description] """ self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts() def get_player_controls(self, channel_index: int, uri: str) -> wx.BoxSizer: outer_sizer = wx.BoxSizer(wx.VERTICAL) inner_sizer = wx.BoxSizer(wx.HORIZONTAL) play_button = wx.Button(self.m_panel, -1, "Play", size=(WIDTH / 4, BTN_HEIGHT)) pause_button = wx.Button(self.m_panel, -1, "Pause", size=(WIDTH / 4, BTN_HEIGHT)) stop_button = wx.Button(self.m_panel, -1, "Stop", size=(WIDTH / 4, BTN_HEIGHT)) if self.m_mode == 'normal': back_button = wx.Button(self.m_panel, -1, "Go Back", size=(WIDTH, BTN_HEIGHT)) back_button.Bind( wx.EVT_BUTTON, lambda event, cindex=channel_index: self.show_video_list( event, cindex), ) else: back_button = wx.Button(self.m_panel, -1, label="Close", size=(WIDTH, BTN_HEIGHT)) back_button.Bind(wx.EVT_BUTTON, lambda event: self.Destroy()) outer_sizer.Add(back_button, FLAGS) inner_sizer.Add(play_button, FLAGS) inner_sizer.Add(pause_button, FLAGS) inner_sizer.Add(stop_button, FLAGS) if self.has_usable_chromecasts(): btm = make_bitmap_from_file( '{}/assets/Cast.png'.format(self.asset_path), wx.Size(24, 24)) cast_button = wx.BitmapButton(self.m_panel, -1, bitmap=btm, size=(WIDTH / 4, BTN_HEIGHT)) cast_button.Bind( wx.EVT_BUTTON, lambda event, muri=uri, cindex=channel_index: self. select_chromecast(event, muri, cindex), ) inner_sizer.Add(cast_button, FLAGS) if self.m_selected_chromecast: self.cast(wx.media.EVT_MEDIA_LOADED, uri), play_button.Bind(wx.EVT_BUTTON, self.play_cast) pause_button.Bind(wx.EVT_BUTTON, self.pause_cast) stop_button.Bind( wx.EVT_BUTTON, lambda event, muri=uri, cindex=channel_index: self. stop_callback(event, muri, cindex), ) else: self.Bind(wx.media.EVT_MEDIA_LOADED, lambda event: wx.Frame.SetFocus(self)) play_button.Bind(wx.EVT_BUTTON, self.play) pause_button.Bind(wx.EVT_BUTTON, self.pause) stop_button.Bind(wx.EVT_BUTTON, self.stop) outer_sizer.Add(inner_sizer, FLAGS) inner_sizer.Fit(self) outer_sizer.Fit(self) outer_sizer.Layout() return outer_sizer def get_providers(self) -> list[ChannelProvider]: providers = list() chandict = { "kanaler": { "channels": [ "ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen", "ch-kunskapskanalen" ], "displayname": "SVT Channel Streams" } } chandict["program"] = { "channels": ["feed", "allprograms"], "displayname": "SVT Shows - Latest and A-Ö" } categories = list() for category in get_all_svt_categories(): categories.append(category['id']) chandict["kategorier"] = { "channels": categories, "displayname": "SVT All Categories" } for provider in ["kanaler", "program", "kategorier"]: channels = list() for id in chandict[provider]["channels"]: channels.append(SVT.SVT(id)) svt = ChannelProvider(chandict[provider]["displayname"], channels=channels) providers.append(svt) return providers def has_usable_chromecasts(self) -> bool: if self.m_chromecast_thr.is_alive(): return False if self.m_selected_chromecast: return False result = False for cast in self.m_chromecasts: if cast.cast_type != 'audio': result = True break return result def show_channel_list(self, _, provider_index) -> None: self.m_selected_provider_index = provider_index self.m_selected_provider = self.m_providers[provider_index] self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) if len(self.m_providers) > 1: back_callback = lambda event: self.show_provider_list(event) self.add_back_button(back_callback) else: closebtn = wx.Button(self.m_panel, -1, label="Close", size=(WIDTH, BTN_HEIGHT)) closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy()) self.m_sizer.Add(closebtn, 0, wx.ALL, 1) channel_index = 0 for channel in self.m_selected_provider.get_channels(): bitmap = channel.get_logo_as_bitmap() callback = lambda event, cindex=channel_index: self.show_video_list( event, cindex) btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap, channel.get_name(), callback) self.m_sizer.Add(btn_sizer) channel_index += 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_provider_list(self, _) -> None: self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) closebtn = wx.Button(self.m_panel, -1, label="Close", size=(WIDTH, BTN_HEIGHT)) closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy()) self.m_sizer.Add(closebtn, 0, wx.ALL, 1) provider_index = 0 for provider in self.m_providers: bitmap = provider.get_logo_as_bitmap() callback = lambda event, pindex=provider_index: self.show_channel_list( event, pindex) btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap, provider.get_name(), callback) self.m_sizer.Add(btn_sizer, 0, wx.ALL, 1) provider_index += 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_video_list(self, _, channel_index) -> None: self.SetWindowStyle(self.m_style) self.m_selected_channel = self.m_selected_provider.get_channel_by_index( channel_index) self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) back_callback = lambda event: self.show_channel_list( event, self.m_selected_provider_index) self.add_back_button(back_callback) if self.m_selected_provider.get_name().startswith( 'SVT') and self.m_selected_channel.get_id() not in [ "ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen", "ch-kunskapskanalen" ]: def refresh_callback(event): self.m_selected_channel.refresh() self.show_video_list(event, channel_index) refreshbtn = wx.Button(self.m_panel, -1, label="Refresh", size=(WIDTH, BTN_HEIGHT)) refreshbtn.Bind(wx.EVT_BUTTON, refresh_callback) self.m_sizer.Add(refreshbtn) if self.m_selected_channel.wait(): with wx.BusyInfo("Please wait, working..."): number_of_waits = 0 while self.m_selected_channel.wait(): number_of_waits += 1 time.sleep(1) wx.GetApp().Yield() if number_of_waits > 10: break btnindex = 0 for item in self.m_selected_channel.get_items(): # type: ignore inner_sizer = wx.BoxSizer(wx.VERTICAL) pane_sizer = wx.BoxSizer(wx.VERTICAL) title = wx.StaticText(self.m_panel, -1) title.SetLabelMarkup("{}".format( item["title"])) bitmap = item["thumbnail"] btn = wx.BitmapButton(self.m_panel, id=btnindex, bitmap=bitmap, style=BM_BTN_STYLE, size=bitmap.GetSize()) btn.Bind( wx.EVT_BUTTON, lambda event, link=item["link"], cindex=channel_index: self. show_player(event, link, cindex), ) inner_sizer.Add(title) inner_sizer.Add(btn) collapsable_pane = wx.CollapsiblePane(self.m_panel, wx.ID_ANY, "Details:", size=(WIDTH, 20)) inner_sizer.Add(collapsable_pane, 0, wx.GROW | wx.ALL, 5) pane_win = collapsable_pane.GetPane() description = wx.StaticText(pane_win, -1, item["description"]) description.Wrap(WIDTH - 2) pane_sizer.Add(description, wx.GROW | wx.ALL, 2) pane_win.SetSizer(pane_sizer) pane_sizer.SetSizeHints(pane_win) def fit_and_layout(_): pane_sizer.Layout() pane_sizer.Fit(pane_win) inner_sizer.Layout() inner_sizer.Fit(self) self.m_sizer.Fit(self) self.m_sizer.Layout() collapsable_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, lambda event: fit_and_layout(event)) #inner_sizer.Add(description) self.m_sizer.Add(inner_sizer) self.m_sizer.AddSpacer(SPACER_HEIGHT) btnindex = btnindex + 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_player(self, _, uri: str, channel_index: int): """ Show the video player :param _ event: unused :param uri str: the link to the video stream """ media = self.m_vlc.media_new(uri) medialist = self.m_vlc.media_list_new() medialist.add_media(media) self.m_vlc_listplayer.set_media_list(medialist) self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.Add(self.get_player_controls(channel_index, uri), FLAGS) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def select_chromecast(self, _, uri, channel_index): self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) cancel_btn = wx.Button(self.m_panel, -1, "Cancel", size=(WIDTH, BTN_HEIGHT)) cancel_btn.Bind( wx.EVT_BUTTON, lambda event, cindex=channel_index: self.show_video_list( event, cindex), ) self.m_sizer.Add(cancel_btn) for cast in self.m_chromecasts: if cast.cast_type == 'audio': continue friendly_name = "Unknown Chromecast" try: friendly_name = cast.device.friendly_name except AttributeError: friendly_name = cast.cast_info.friendly_name btn = wx.Button(self.m_panel, id=-1, label=friendly_name, size=(WIDTH, BTN_HEIGHT)) btn.Bind( wx.EVT_BUTTON, lambda event, chromecast=cast, muri=uri, cindex=channel_index: self.set_chromecast(event, chromecast, muri, cindex), ) self.m_sizer.Add(btn) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def set_chromecast(self, event, chromecast, uri, channel_index): self.m_selected_chromecast = chromecast self.show_player(event, uri, channel_index) def cast(self, _, uri): mimetype = "video/mp4" cast = self.m_selected_chromecast cast.wait() cast.media_controller.play_media(uri, mimetype) # Wait for player_state PLAYING player_state = None has_played = False while True: if player_state != cast.media_controller.status.player_state: player_state = cast.media_controller.status.player_state if player_state == "PLAYING": has_played = True break if (cast.socket_client.is_connected and has_played and player_state != "PLAYING"): has_played = False cast.media_controller.play_media(uri, mimetype) time.sleep(0.1) self.m_browser.stop_discovery() def pause_cast(self, event): cast = self.m_selected_chromecast cast.wait() cast.media_controller.pause() def play_cast(self, event): cast = self.m_selected_chromecast cast.wait() cast.media_controller.play() def stop_cast(self, event): cast = self.m_selected_chromecast cast.wait() cast.media_controller.pause() time.sleep(2) cast.media_controller.stop() self.m_selected_chromecast = None def stop_callback(self, event, uri: str, channel_index: int): self.stop_cast(event) self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.Add(self.get_player_controls(channel_index, uri)) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def play(self, _): self.m_vlc_listplayer.play() def pause(self, _): self.m_vlc_listplayer.pause() def stop(self, _): self.m_vlc_listplayer.stop() def quit(self, _): self.Destroy() if __name__ == "__main__": # When this module is run (not imported) then create the app, the # frame, show it, and start the event loop. url = None app: wx.App = wx.App() if len(sys.argv) > 1: url = sys.argv[1] frm: Cast = Cast(None, title="Cast", url=url) else: frm: Cast = Cast(None, title="Cast") frm.Show() app.MainLoop()