diff --git a/config.txt b/config.txt index 31670da9..6fe12d16 100644 --- a/config.txt +++ b/config.txt @@ -36,6 +36,8 @@ folder.images = folder.jpg, folder.png, cover.jpg, cover.png, front.jpg, front.p cover.art.folders = covers, artwork, scans, art auto.play.next.track = True cyclic.playback = True +hide.folder.name = False +folder.image.scale.ratio = 0.8 [web.server] http.port = 8000 @@ -43,12 +45,16 @@ http.port = 8000 [stream.server] stream.server.port = 8080 +[podcasts] +podcasts.folder = C:\temp\podcasts + [home.menu] radio = True audio-files = True audiobooks = True -stream = True +stream = False cd-player = False +podcasts = True equalizer = True timer = True @@ -80,3 +86,7 @@ color.mute = 242,107,106 [font] font.name = FiraSans.ttf + +[scripts] +startup.script.name = startup.py +shutdown.script.name = shutdown.py diff --git a/current.txt b/current.txt index f7a58913..eeefb693 100644 --- a/current.txt +++ b/current.txt @@ -29,6 +29,12 @@ book.url = book.track.filename = book.time = +[podcasts] +podcast.url = +podcast.episode.name = +podcast.episode.url = +podcast.episode.time = + [screensaver] name = delay = diff --git a/icons/loading.svg b/icons/loading.svg new file mode 100644 index 00000000..699310b4 --- /dev/null +++ b/icons/loading.svg @@ -0,0 +1,949 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/podcasts-menu.svg b/icons/podcasts-menu.svg new file mode 100644 index 00000000..489b985f --- /dev/null +++ b/icons/podcasts-menu.svg @@ -0,0 +1,950 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/podcasts.svg b/icons/podcasts.svg new file mode 100644 index 00000000..c691e8df --- /dev/null +++ b/icons/podcasts.svg @@ -0,0 +1,959 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/languages/English-USA/labels.properties b/languages/English-USA/labels.properties index 41d7e130..3b074eb6 100644 --- a/languages/English-USA/labels.properties +++ b/languages/English-USA/labels.properties @@ -8,6 +8,7 @@ audio-files = Audio Files stream = Stream audiobooks = Audiobooks cd-player = CD Player +podcasts = Podcasts equalizer = Equalizer timer = Timer diff --git a/languages/French/labels.properties b/languages/French/labels.properties index 244314ac..d25daf43 100644 --- a/languages/French/labels.properties +++ b/languages/French/labels.properties @@ -8,6 +8,7 @@ audio-files = Fichiers Audio stream = Courant audiobooks = Livres Audio cd-player = Lecteur CD +podcasts = Podcasts equalizer = Égaliseur timer = Minuteur diff --git a/languages/German/labels.properties b/languages/German/labels.properties index 9b9973e2..56ec275a 100644 --- a/languages/German/labels.properties +++ b/languages/German/labels.properties @@ -8,6 +8,7 @@ audio-files = Audiodateien stream = Stream audiobooks = Hörbücher cd-player = CD-Player +podcasts = Podcasts equalizer = Equalizer timer = Timer diff --git a/languages/Russian/labels.properties b/languages/Russian/labels.properties index 63205957..3bf285bc 100644 --- a/languages/Russian/labels.properties +++ b/languages/Russian/labels.properties @@ -8,6 +8,7 @@ audio-files = Аудио Файлы stream = Поток audiobooks = Аудиокниги cd-player = CD Плеер +podcasts = Подкасты equalizer = Эквалайзер timer = Таймер diff --git a/peppy.py b/peppy.py index 73cf7b5d..1b6623bf 100644 --- a/peppy.py +++ b/peppy.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Peppy Player peppy.player@gmail.com +# Copyright 2016-2019 Peppy Player peppy.player@gmail.com # # This file is part of Peppy Player. # @@ -63,6 +63,9 @@ from websiteparser.loyalbooks.constants import LANGUAGE_PREFIX, ENGLISH_USA, RUSSIAN from websiteparser.siteparser import BOOK_URL, FILE_NAME from util.cdutil import CdUtil +from ui.screen.podcasts import PodcastsScreen +from ui.screen.podcastepisodes import PodcastEpisodesScreen +from ui.screen.podcastplayer import PodcastPlayerScreen class Peppy(object): """ Main class """ @@ -78,6 +81,10 @@ def __init__(self): self.cdutil = CdUtil(self.util) self.use_web = self.config[USAGE][USE_WEB] + s = self.config[SCRIPTS][STARTUP] + if s != None and len(s.strip()) != 0: + self.util.run_script(s) + layout = BorderLayout(self.config[SCREEN_RECT]) layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_TOP_HEIGHT, 0, 0) self.config[MAXIMUM_FONT_SIZE] = int((layout.TOP.h * PERCENT_TITLE_FONT)/100.0) @@ -137,7 +144,8 @@ def __init__(self): self.event_dispatcher = EventDispatcher(self.screensaver_dispatcher, self.util) self.current_screen = None - self.PLAYER_SCREENS = [KEY_STATIONS, STREAM, KEY_PLAY_FILE, KEY_PLAY_CD] + self.current_mode = self.config[CURRENT][MODE] + self.PLAYER_SCREENS = [KEY_STATIONS, STREAM, KEY_PLAY_FILE, KEY_PLAY_CD, KEY_PODCAST_PLAYER] if not self.config[CURRENT][MODE] or not self.config[USAGE][USE_AUTO_PLAY]: self.go_home(None) @@ -160,11 +168,19 @@ def __init__(self): self.go_site_playback(state) elif self.config[CURRENT][MODE] == CD_PLAYER: self.go_cd_playback() + elif self.config[CURRENT][MODE] == PODCASTS: + state = State() + state.podcast_url = self.config[PODCASTS][PODCAST_URL] + state.name = self.config[PODCASTS][PODCAST_EPISODE_NAME] + state.url = self.config[PODCASTS][PODCAST_EPISODE_URL] + state.episode_time = self.config[PODCASTS][PODCAST_EPISODE_TIME] + state.source = INIT + self.go_podcast_player(state) self.player_state = PLAYER_RUNNING self.run_timer_thread = False self.start_timer_thread() - + def check_internet_connectivity(self): """ Exit if Internet is not available after 3 attempts 3 seconds each """ @@ -378,8 +394,11 @@ def set_mode(self, state): """ self.store_current_track_time(self.current_screen) - mode = state.name - self.player.stop() + mode = state.genre + + if self.current_mode != mode: + self.player.stop() + self.current_mode = mode if mode == RADIO: self.go_stations(state) @@ -387,7 +406,8 @@ def set_mode(self, state): elif mode == STREAM: self.go_stream(state) elif mode == AUDIOBOOKS: self.go_audiobooks(state) elif mode == CD_PLAYER: self.go_cd_playback(state) - + elif mode == PODCASTS: self.go_podcast_player(state) + def go_player(self, state): """ Go to the current player screen @@ -404,7 +424,9 @@ def go_player(self, state): elif self.current_player_screen == KEY_PLAY_CD: self.go_cd_playback(state) elif self.current_player_screen == STREAM: - self.go_stream(state) + self.go_stream(state) + elif self.current_player_screen == KEY_PODCAST_PLAYER: + self.go_podcast_player(state) def go_favorites(self, state): """ Go to the favorites screen @@ -1129,6 +1151,122 @@ def go_stream(self, state=None): stream_screen.station_menu.add_menu_click_listener(self.web_server.station_menu_to_json) stream_screen.station_menu.add_mode_listener(self.web_server.station_menu_to_json) + def go_podcasts(self, state=None): + """ Go to the Podcasts Screen + + :param state: button state + """ + if self.get_current_screen(PODCASTS): return + + try: + if self.screens[PODCASTS]: + self.set_current_screen(PODCASTS, state=state) + return + except: + pass + + listeners = {} + listeners[KEY_HOME] = self.go_home + listeners[KEY_PLAYER] = self.go_podcast_player + listeners[GO_BACK] = self.go_back + listeners[KEY_PODCAST_EPISODES] = self.go_podcast_episodes + podcasts_screen = PodcastsScreen(self.util, listeners, self.voice_assistant) + self.screens[PODCASTS] = podcasts_screen + if self.use_web: + self.add_screen_observers(podcasts_screen) + self.set_current_screen(PODCASTS) + + def go_podcast_episodes(self, state): + """ Go to the podcast episodes screen + + :param state: button state + """ + url = getattr(state, "podcast_url", None) + + if url != None: + self.config[PODCASTS][PODCAST_URL] = url + + try: + if self.screens[KEY_PODCAST_EPISODES]: + self.set_current_screen(KEY_PODCAST_EPISODES, state=state) + return + except: + if state and hasattr(state, "name") and len(state.name) == 0: + self.go_podcasts(state) + return + + listeners = {} + listeners[KEY_HOME] = self.go_home + listeners[PODCASTS] = self.go_podcasts + listeners[GO_BACK] = self.go_back + listeners[KEY_PLAYER] = self.go_podcast_player + screen = PodcastEpisodesScreen(self.util, listeners, self.voice_assistant, state) + self.screens[KEY_PODCAST_EPISODES] = screen + + podcast_player = self.screens[KEY_PODCAST_PLAYER] + podcast_player.add_play_listener(screen.turn_page) + if self.use_web: + self.add_screen_observers(screen) + self.set_current_screen(KEY_PODCAST_EPISODES, state=state) + + def go_podcast_player(self, state): + """ Go to the podcast player screen + + :param state: button state + """ + self.deactivate_current_player(KEY_PODCAST_PLAYER) + try: + if self.screens[KEY_PODCAST_PLAYER]: + if getattr(state, "name", None) and (state.name == KEY_HOME or state.name == KEY_BACK): + self.set_current_screen(KEY_PODCAST_PLAYER) + else: + if getattr(state, "source", None) == None: + state.source = RESUME + self.set_current_screen(name=KEY_PODCAST_PLAYER, state=state) + self.current_player_screen = KEY_PODCAST_PLAYER + return + except: + pass + + if state.name != PODCASTS: + self.config[PODCASTS][PODCAST_EPISODE_NAME] = state.name + + if hasattr(state, "file_name"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.file_name + elif hasattr(state, "url"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.url + + listeners = self.get_play_screen_listeners() + listeners[AUDIO_FILES] = self.go_podcast_episodes + listeners[KEY_SEEK] = self.player.seek + screen = PodcastPlayerScreen(listeners, self.util, self.player.get_current_playlist, self.voice_assistant, self.player.stop) + self.screens[KEY_PODCAST_PLAYER] = screen + self.current_player_screen = KEY_PODCAST_PLAYER + screen.load_playlist = self.player.load_playlist + + self.player.add_player_listener(screen.time_control.set_track_info) + self.player.add_player_listener(screen.update_arrow_button_labels) + self.player.add_end_of_track_listener(screen.end_of_track) + + screen.add_play_listener(self.screensaver_dispatcher.change_image) + screen.add_play_listener(self.screensaver_dispatcher.change_image_folder) + + self.set_current_screen(KEY_PODCAST_PLAYER, state=state) + state = State() + state.cover_art_folder = screen.file_button.state.cover_art_folder + self.screensaver_dispatcher.change_image_folder(state) + + if self.use_web: + update = self.web_server.update_web_ui + redraw = self.web_server.redraw_web_ui + start = self.web_server.start_time_control_to_json + stop = self.web_server.stop_time_control_to_json + title_to_json = self.web_server.title_to_json + screen.add_screen_observers(update, redraw, start, stop, title_to_json) + screen.add_loading_listener(redraw) + self.web_server.add_player_listener(screen.time_control) + self.player.add_player_listener(self.web_server.update_player_listeners) + def go_audiobooks(self, state=None): """ Go to the Audiobooks Screen @@ -1358,7 +1496,7 @@ def set_current_screen(self, name, go_back=False, state=None): if src == GENRE or src == KEY_FAVORITES: new_genre = True new_language = self.current_language != self.config[CURRENT][LANGUAGE] - if new_genre or new_language or self.current_player_screen != name: + if new_genre or new_language or self.current_player_screen != name or self.current_mode != name: self.current_language = self.config[CURRENT][LANGUAGE] cs.set_current(state=state) elif name == KEY_PLAY_FILE: @@ -1404,7 +1542,34 @@ def set_current_screen(self, name, go_back=False, state=None): cs.set_current(state) elif name == KEY_CD_TRACKS: cs.set_current(state) - + elif name == PODCASTS or name == KEY_PODCAST_EPISODES: + cs.set_current(state) + elif name == KEY_PODCAST_PLAYER: + f = getattr(state, "file_name", None) + if f and self.current_audio_file != f or self.current_player_screen != name or state.source == INIT: + self.current_audio_file = f + source = getattr(state, "source", None) + if source == "episode_menu": + cs.set_current(state=state, new_track=True) + elif source == RESUME: + s = State() + s.name = self.config[PODCASTS][PODCAST_EPISODE_NAME] + s.url = self.config[PODCASTS][PODCAST_EPISODE_URL] + s.podcast_url = self.config[PODCASTS][PODCAST_URL] + podcasts_util = self.util.get_podcasts_util() + s.podcast_image_url = podcasts_util.summary_cache[s.podcast_url].episodes[0].podcast_image_url + s.status = podcasts_util.get_episode_status(s.podcast_url, s.url) + if self.util.connected_to_internet: + s.online = True + else: + s.online = False + cs.set_current(state=s) + else: + cs.set_current(state=state) + else: + source = getattr(state, "source", None) + if source == RESUME: + cs.start_timer() cs.clean_draw_update() self.event_dispatcher.set_current_screen(cs) self.set_volume() @@ -1481,6 +1646,8 @@ def store_current_track_time(self, mode): k = KEY_PLAY_SITE elif self.current_player_screen == KEY_PLAY_CD: k = KEY_PLAY_CD + elif self.current_player_screen == KEY_PODCAST_PLAYER: + k = KEY_PODCAST_PLAYER if k and k in self.screens: s = self.screens[k] @@ -1493,15 +1660,21 @@ def store_current_track_time(self, mode): self.config[FILE_PLAYBACK][CURRENT_TRACK_TIME] = t elif ps == KEY_PLAY_CD: self.config[CD_PLAYBACK][CD_TRACK_TIME] = t + elif ps == KEY_PODCAST_PLAYER: + self.config[PODCASTS][PODCAST_EPISODE_TIME] = t def shutdown(self, event=None): """ System shutdown handler :param event: the event """ - self.store_current_track_time(self.config[CURRENT][MODE]) - + s = self.config[SCRIPTS][SHUTDOWN] + if s != None and len(s.strip()) != 0: + self.util.run_script(s) + + self.store_current_track_time(self.config[CURRENT][MODE]) self.util.config_class.save_current_settings() + self.player.shutdown() if self.use_web: diff --git a/peppy.pyw b/peppy.pyw index 73cf7b5d..1b6623bf 100644 --- a/peppy.pyw +++ b/peppy.pyw @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Peppy Player peppy.player@gmail.com +# Copyright 2016-2019 Peppy Player peppy.player@gmail.com # # This file is part of Peppy Player. # @@ -63,6 +63,9 @@ from websiteparser.audioknigi.constants import AUDIOKNIGI_ROWS, AUDIOKNIGI_COLUM from websiteparser.loyalbooks.constants import LANGUAGE_PREFIX, ENGLISH_USA, RUSSIAN from websiteparser.siteparser import BOOK_URL, FILE_NAME from util.cdutil import CdUtil +from ui.screen.podcasts import PodcastsScreen +from ui.screen.podcastepisodes import PodcastEpisodesScreen +from ui.screen.podcastplayer import PodcastPlayerScreen class Peppy(object): """ Main class """ @@ -78,6 +81,10 @@ class Peppy(object): self.cdutil = CdUtil(self.util) self.use_web = self.config[USAGE][USE_WEB] + s = self.config[SCRIPTS][STARTUP] + if s != None and len(s.strip()) != 0: + self.util.run_script(s) + layout = BorderLayout(self.config[SCREEN_RECT]) layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_TOP_HEIGHT, 0, 0) self.config[MAXIMUM_FONT_SIZE] = int((layout.TOP.h * PERCENT_TITLE_FONT)/100.0) @@ -137,7 +144,8 @@ class Peppy(object): self.event_dispatcher = EventDispatcher(self.screensaver_dispatcher, self.util) self.current_screen = None - self.PLAYER_SCREENS = [KEY_STATIONS, STREAM, KEY_PLAY_FILE, KEY_PLAY_CD] + self.current_mode = self.config[CURRENT][MODE] + self.PLAYER_SCREENS = [KEY_STATIONS, STREAM, KEY_PLAY_FILE, KEY_PLAY_CD, KEY_PODCAST_PLAYER] if not self.config[CURRENT][MODE] or not self.config[USAGE][USE_AUTO_PLAY]: self.go_home(None) @@ -160,11 +168,19 @@ class Peppy(object): self.go_site_playback(state) elif self.config[CURRENT][MODE] == CD_PLAYER: self.go_cd_playback() + elif self.config[CURRENT][MODE] == PODCASTS: + state = State() + state.podcast_url = self.config[PODCASTS][PODCAST_URL] + state.name = self.config[PODCASTS][PODCAST_EPISODE_NAME] + state.url = self.config[PODCASTS][PODCAST_EPISODE_URL] + state.episode_time = self.config[PODCASTS][PODCAST_EPISODE_TIME] + state.source = INIT + self.go_podcast_player(state) self.player_state = PLAYER_RUNNING self.run_timer_thread = False self.start_timer_thread() - + def check_internet_connectivity(self): """ Exit if Internet is not available after 3 attempts 3 seconds each """ @@ -378,8 +394,11 @@ class Peppy(object): """ self.store_current_track_time(self.current_screen) - mode = state.name - self.player.stop() + mode = state.genre + + if self.current_mode != mode: + self.player.stop() + self.current_mode = mode if mode == RADIO: self.go_stations(state) @@ -387,7 +406,8 @@ class Peppy(object): elif mode == STREAM: self.go_stream(state) elif mode == AUDIOBOOKS: self.go_audiobooks(state) elif mode == CD_PLAYER: self.go_cd_playback(state) - + elif mode == PODCASTS: self.go_podcast_player(state) + def go_player(self, state): """ Go to the current player screen @@ -404,7 +424,9 @@ class Peppy(object): elif self.current_player_screen == KEY_PLAY_CD: self.go_cd_playback(state) elif self.current_player_screen == STREAM: - self.go_stream(state) + self.go_stream(state) + elif self.current_player_screen == KEY_PODCAST_PLAYER: + self.go_podcast_player(state) def go_favorites(self, state): """ Go to the favorites screen @@ -1129,6 +1151,122 @@ class Peppy(object): stream_screen.station_menu.add_menu_click_listener(self.web_server.station_menu_to_json) stream_screen.station_menu.add_mode_listener(self.web_server.station_menu_to_json) + def go_podcasts(self, state=None): + """ Go to the Podcasts Screen + + :param state: button state + """ + if self.get_current_screen(PODCASTS): return + + try: + if self.screens[PODCASTS]: + self.set_current_screen(PODCASTS, state=state) + return + except: + pass + + listeners = {} + listeners[KEY_HOME] = self.go_home + listeners[KEY_PLAYER] = self.go_podcast_player + listeners[GO_BACK] = self.go_back + listeners[KEY_PODCAST_EPISODES] = self.go_podcast_episodes + podcasts_screen = PodcastsScreen(self.util, listeners, self.voice_assistant) + self.screens[PODCASTS] = podcasts_screen + if self.use_web: + self.add_screen_observers(podcasts_screen) + self.set_current_screen(PODCASTS) + + def go_podcast_episodes(self, state): + """ Go to the podcast episodes screen + + :param state: button state + """ + url = getattr(state, "podcast_url", None) + + if url != None: + self.config[PODCASTS][PODCAST_URL] = url + + try: + if self.screens[KEY_PODCAST_EPISODES]: + self.set_current_screen(KEY_PODCAST_EPISODES, state=state) + return + except: + if state and hasattr(state, "name") and len(state.name) == 0: + self.go_podcasts(state) + return + + listeners = {} + listeners[KEY_HOME] = self.go_home + listeners[PODCASTS] = self.go_podcasts + listeners[GO_BACK] = self.go_back + listeners[KEY_PLAYER] = self.go_podcast_player + screen = PodcastEpisodesScreen(self.util, listeners, self.voice_assistant, state) + self.screens[KEY_PODCAST_EPISODES] = screen + + podcast_player = self.screens[KEY_PODCAST_PLAYER] + podcast_player.add_play_listener(screen.turn_page) + if self.use_web: + self.add_screen_observers(screen) + self.set_current_screen(KEY_PODCAST_EPISODES, state=state) + + def go_podcast_player(self, state): + """ Go to the podcast player screen + + :param state: button state + """ + self.deactivate_current_player(KEY_PODCAST_PLAYER) + try: + if self.screens[KEY_PODCAST_PLAYER]: + if getattr(state, "name", None) and (state.name == KEY_HOME or state.name == KEY_BACK): + self.set_current_screen(KEY_PODCAST_PLAYER) + else: + if getattr(state, "source", None) == None: + state.source = RESUME + self.set_current_screen(name=KEY_PODCAST_PLAYER, state=state) + self.current_player_screen = KEY_PODCAST_PLAYER + return + except: + pass + + if state.name != PODCASTS: + self.config[PODCASTS][PODCAST_EPISODE_NAME] = state.name + + if hasattr(state, "file_name"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.file_name + elif hasattr(state, "url"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.url + + listeners = self.get_play_screen_listeners() + listeners[AUDIO_FILES] = self.go_podcast_episodes + listeners[KEY_SEEK] = self.player.seek + screen = PodcastPlayerScreen(listeners, self.util, self.player.get_current_playlist, self.voice_assistant, self.player.stop) + self.screens[KEY_PODCAST_PLAYER] = screen + self.current_player_screen = KEY_PODCAST_PLAYER + screen.load_playlist = self.player.load_playlist + + self.player.add_player_listener(screen.time_control.set_track_info) + self.player.add_player_listener(screen.update_arrow_button_labels) + self.player.add_end_of_track_listener(screen.end_of_track) + + screen.add_play_listener(self.screensaver_dispatcher.change_image) + screen.add_play_listener(self.screensaver_dispatcher.change_image_folder) + + self.set_current_screen(KEY_PODCAST_PLAYER, state=state) + state = State() + state.cover_art_folder = screen.file_button.state.cover_art_folder + self.screensaver_dispatcher.change_image_folder(state) + + if self.use_web: + update = self.web_server.update_web_ui + redraw = self.web_server.redraw_web_ui + start = self.web_server.start_time_control_to_json + stop = self.web_server.stop_time_control_to_json + title_to_json = self.web_server.title_to_json + screen.add_screen_observers(update, redraw, start, stop, title_to_json) + screen.add_loading_listener(redraw) + self.web_server.add_player_listener(screen.time_control) + self.player.add_player_listener(self.web_server.update_player_listeners) + def go_audiobooks(self, state=None): """ Go to the Audiobooks Screen @@ -1358,7 +1496,7 @@ class Peppy(object): if src == GENRE or src == KEY_FAVORITES: new_genre = True new_language = self.current_language != self.config[CURRENT][LANGUAGE] - if new_genre or new_language or self.current_player_screen != name: + if new_genre or new_language or self.current_player_screen != name or self.current_mode != name: self.current_language = self.config[CURRENT][LANGUAGE] cs.set_current(state=state) elif name == KEY_PLAY_FILE: @@ -1404,7 +1542,34 @@ class Peppy(object): cs.set_current(state) elif name == KEY_CD_TRACKS: cs.set_current(state) - + elif name == PODCASTS or name == KEY_PODCAST_EPISODES: + cs.set_current(state) + elif name == KEY_PODCAST_PLAYER: + f = getattr(state, "file_name", None) + if f and self.current_audio_file != f or self.current_player_screen != name or state.source == INIT: + self.current_audio_file = f + source = getattr(state, "source", None) + if source == "episode_menu": + cs.set_current(state=state, new_track=True) + elif source == RESUME: + s = State() + s.name = self.config[PODCASTS][PODCAST_EPISODE_NAME] + s.url = self.config[PODCASTS][PODCAST_EPISODE_URL] + s.podcast_url = self.config[PODCASTS][PODCAST_URL] + podcasts_util = self.util.get_podcasts_util() + s.podcast_image_url = podcasts_util.summary_cache[s.podcast_url].episodes[0].podcast_image_url + s.status = podcasts_util.get_episode_status(s.podcast_url, s.url) + if self.util.connected_to_internet: + s.online = True + else: + s.online = False + cs.set_current(state=s) + else: + cs.set_current(state=state) + else: + source = getattr(state, "source", None) + if source == RESUME: + cs.start_timer() cs.clean_draw_update() self.event_dispatcher.set_current_screen(cs) self.set_volume() @@ -1481,6 +1646,8 @@ class Peppy(object): k = KEY_PLAY_SITE elif self.current_player_screen == KEY_PLAY_CD: k = KEY_PLAY_CD + elif self.current_player_screen == KEY_PODCAST_PLAYER: + k = KEY_PODCAST_PLAYER if k and k in self.screens: s = self.screens[k] @@ -1493,15 +1660,21 @@ class Peppy(object): self.config[FILE_PLAYBACK][CURRENT_TRACK_TIME] = t elif ps == KEY_PLAY_CD: self.config[CD_PLAYBACK][CD_TRACK_TIME] = t + elif ps == KEY_PODCAST_PLAYER: + self.config[PODCASTS][PODCAST_EPISODE_TIME] = t def shutdown(self, event=None): """ System shutdown handler :param event: the event """ - self.store_current_track_time(self.config[CURRENT][MODE]) - + s = self.config[SCRIPTS][SHUTDOWN] + if s != None and len(s.strip()) != 0: + self.util.run_script(s) + + self.store_current_track_time(self.config[CURRENT][MODE]) self.util.config_class.save_current_settings() + self.player.shutdown() if self.use_web: diff --git a/player/client/vlcclient.py b/player/client/vlcclient.py index 6c56d360..29457d86 100644 --- a/player/client/vlcclient.py +++ b/player/client/vlcclient.py @@ -175,8 +175,11 @@ def play(self, state): self.media = self.instance.media_new(url) self.player.set_media(self.media) - self.player.play() - self.player.set_time(int(float(self.seek_time)) * 1000) + self.player.play() + try: + self.player.set_time(int(float(self.seek_time)) * 1000) + except Exception as e: + pass if getattr(state, "volume", None): self.set_volume(int(state.volume)) diff --git a/podcasts/podcasts.m3u b/podcasts/podcasts.m3u new file mode 100644 index 00000000..daf3fccd --- /dev/null +++ b/podcasts/podcasts.m3u @@ -0,0 +1,14 @@ +http://feeds.wnyc.org/aria-code +http://www.classical-music.com/sites/default/files/playlists/BBC_Music.xml +https://podcasts.nac-cna.ca/ExploreTheSymphony +https://podcasts.nac-cna.ca/NACOcast +https://www.listennotes.com/c/r/857088d808424db482e24cc3efa243cf +https://indianapublicmedia.org/harmonia/feed/podcast/ +https://feed.pippa.io/public/shows/thatclassicalpodcast +https://rss.acast.com/barbicanclassicalpodcast +http://feeds.soundcloud.com/users/soundcloud:users:12134481/sounds.rss +http://www.indieopera.com/rss/indieopera.rss +https://www.classicsforkids.com/podcasts/classicsforkids.xml +https://www.listennotes.com/c/r/11d7620fb3964020b0d6f906c1dbaf01 +https://www.listennotes.com/c/r/d2c5417aefe44752908753bdd7090155 +https://www.listennotes.com/c/r/1611708b156042ec824b24796bae5466 diff --git a/screensaver/peppymeter/config.txt b/screensaver/peppymeter/config.txt index 704758a0..e4d1b333 100644 --- a/screensaver/peppymeter/config.txt +++ b/screensaver/peppymeter/config.txt @@ -5,6 +5,7 @@ screen.size = medium output.display = True output.serial = False output.i2c = False +output.pwm = False use.logging = False [serial.interface] @@ -20,6 +21,12 @@ right.channel.address = 0x20 output.size = 10 update.period = 0.1 +[pwm.interface] +frequency = 500 +gpio.pin.left = 24 +gpio.pin.right = 25 +update.period = 0.1 + [data.source] type = pipe polling.interval = 0.01 diff --git a/screensaver/peppymeter/configfileparser.py b/screensaver/peppymeter/configfileparser.py index f4411a19..a98fe12b 100644 --- a/screensaver/peppymeter/configfileparser.py +++ b/screensaver/peppymeter/configfileparser.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 PeppyMeter peppy.player@gmail.com +# Copyright 2016-2019 PeppyMeter peppy.player@gmail.com # # This file is part of PeppyMeter. # @@ -32,17 +32,25 @@ OUTPUT_DISPLAY = "output.display" OUTPUT_SERIAL = "output.serial" OUTPUT_I2C = "output.i2c" +OUTPUT_PWM = "output.pwm" + SERIAL_INTERFACE = "serial.interface" DEVICE_NAME = "device.name" BAUD_RATE = "baud.rate" INCLUDE_TIME = "include.time" UPDATE_PERIOD = "update.period" + I2C_INTERFACE = "i2c.interface" PORT = "port" LEFT_CHANNEL_ADDRESS = "left.channel.address" RIGHT_CHANNEL_ADDRESS = "right.channel.address" OUTPUT_SIZE = "output.size" -USAGE = "usage" + +PWM_INTERFACE = "pwm.interface" +FREQUENCY = "frequency" +GPIO_PIN_LEFT = "gpio.pin.left" +GPIO_PIN_RIGHT = "gpio.pin.right" + USE_LOGGING = "use.logging" USE_VU_METER = "use.vu.meter" METER = "meter" @@ -128,6 +136,7 @@ def __init__(self, base_path): self.meter_config[OUTPUT_DISPLAY] = c.getboolean(CURRENT, OUTPUT_DISPLAY) self.meter_config[OUTPUT_SERIAL] = c.getboolean(CURRENT, OUTPUT_SERIAL) self.meter_config[OUTPUT_I2C] = c.getboolean(CURRENT, OUTPUT_I2C) + self.meter_config[OUTPUT_PWM] = c.getboolean(CURRENT, OUTPUT_PWM) self.meter_config[USE_LOGGING] = c.getboolean(CURRENT, USE_LOGGING) self.meter_config[SERIAL_INTERFACE] = {} @@ -142,6 +151,12 @@ def __init__(self, base_path): self.meter_config[I2C_INTERFACE][RIGHT_CHANNEL_ADDRESS] = int(c.get(I2C_INTERFACE, RIGHT_CHANNEL_ADDRESS), 0) self.meter_config[I2C_INTERFACE][OUTPUT_SIZE] = c.getint(I2C_INTERFACE, OUTPUT_SIZE) self.meter_config[I2C_INTERFACE][UPDATE_PERIOD] = c.getfloat(I2C_INTERFACE, UPDATE_PERIOD) + + self.meter_config[PWM_INTERFACE] = {} + self.meter_config[PWM_INTERFACE][FREQUENCY] = c.getint(PWM_INTERFACE, FREQUENCY) + self.meter_config[PWM_INTERFACE][GPIO_PIN_LEFT] = c.getint(PWM_INTERFACE, GPIO_PIN_LEFT) + self.meter_config[PWM_INTERFACE][GPIO_PIN_RIGHT] = c.getint(PWM_INTERFACE, GPIO_PIN_RIGHT) + self.meter_config[PWM_INTERFACE][UPDATE_PERIOD] = c.getfloat(PWM_INTERFACE, UPDATE_PERIOD) screen_size = c.get(CURRENT, SCREEN_SIZE) self.meter_config[SCREEN_INFO] = {} diff --git a/screensaver/peppymeter/datasource.py b/screensaver/peppymeter/datasource.py index 09cb0fdc..61b6fc50 100644 --- a/screensaver/peppymeter/datasource.py +++ b/screensaver/peppymeter/datasource.py @@ -23,6 +23,7 @@ from random import uniform from threading import Thread, RLock from configfileparser import * +from util.config import VOLUME, PLAYER_SETTINGS SOURCE_CONSTANT = "constant" SOURCE_NOISE = "noise" @@ -43,11 +44,13 @@ class DataSource(object): lock = RLock() - def __init__(self, c): + def __init__(self, util): """ Initializer :param c: configuration dictionary """ + c = util.meter_config + self.volume = util.config[PLAYER_SETTINGS][VOLUME] self.config = c[DATA_SOURCE] self.mono_algorithm = self.config[MONO_ALGORITHM] self.stereo_algorithm = self.config[STEREO_ALGORITHM] @@ -198,6 +201,9 @@ def get_pipe_value(self): data = None left = right = mono = 0.0 + volume_level = self.volume + if volume_level == 0: + volume_level = 1 if self.pipe == None: return (left, right, mono) @@ -208,8 +214,8 @@ def get_pipe_value(self): if length == 0: return (self.previous_left, self.previous_right, self.previous_mono) - new_left = int(self.max_in_ui * ((data[length - 4] + (data[length - 3] << 8)) / self.max_in_pipe)) - new_right = int(self.max_in_ui * ((data[length - 2] + (data[length - 1] << 8)) / self.max_in_pipe)) + new_left = int(10 * self.max_in_ui * ((data[length - 4] + (data[length - 3] << 8)) / (volume_level**3))) + new_right = int(10 * self.max_in_ui * ((data[length - 2] + (data[length - 1] << 8)) / (volume_level**3))) new_mono = self.get_mono(new_left, new_right) left = self.get_channel(self.previous_left, new_left) diff --git a/screensaver/peppymeter/peppymeter.py b/screensaver/peppymeter/peppymeter.py index 85fdd3d2..3f2dfa7a 100644 --- a/screensaver/peppymeter/peppymeter.py +++ b/screensaver/peppymeter/peppymeter.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 PeppyMeter peppy.player@gmail.com +# Copyright 2016-2019 PeppyMeter peppy.player@gmail.com # # This file is part of PeppyMeter. # @@ -26,9 +26,10 @@ from datasource import DataSource, SOURCE_NOISE, SOURCE_PIPE from serialinterface import SerialInterface from i2cinterface import I2CInterface +from pwminterface import PWMInterface from screensavermeter import ScreensaverMeter from configfileparser import ConfigFileParser, SCREEN_RECT, SCREEN_INFO, WIDTH, HEIGHT, DEPTH, \ - OUTPUT_DISPLAY, OUTPUT_SERIAL, OUTPUT_I2C, DATA_SOURCE, TYPE, USAGE, USE_LOGGING, USE_VU_METER + OUTPUT_DISPLAY, OUTPUT_SERIAL, OUTPUT_I2C, OUTPUT_PWM, DATA_SOURCE, TYPE, USE_LOGGING, USE_VU_METER class Peppymeter(ScreensaverMeter): """ Peppy Meter class """ @@ -44,11 +45,8 @@ def __init__(self, util=None, standalone=False): self.util = util else: self.util = MeterUtil() - - try: - self.use_vu_meter = self.util.config[USAGE][USE_VU_METER] - except: - self.use_vu_meter = False + + use_vu_meter = getattr(self.util, USE_VU_METER, None) base_path = "." if __package__: @@ -67,13 +65,13 @@ def __init__(self, util=None, standalone=False): logging.disable(logging.CRITICAL) # no VU Meter support for Windows - if "win" in sys.platform: - self.util.meter_config[DATA_SOURCE][TYPE] = SOURCE_NOISE + if "win" in sys.platform or use_vu_meter == False: + if self.util.meter_config[DATA_SOURCE][TYPE] == SOURCE_PIPE: + self.util.meter_config[DATA_SOURCE][TYPE] = SOURCE_NOISE - self.data_source = DataSource(self.util.meter_config) - if self.util.meter_config[DATA_SOURCE][TYPE] == SOURCE_PIPE and self.use_vu_meter == True: + self.data_source = DataSource(self.util) + if self.util.meter_config[DATA_SOURCE][TYPE] == SOURCE_PIPE or use_vu_meter == True: self.data_source.start_data_source() - logging.debug("started datasource in init") if self.util.meter_config[OUTPUT_DISPLAY]: self.meter = self.output_display(self.data_source) @@ -83,6 +81,9 @@ def __init__(self, util=None, standalone=False): if self.util.meter_config[OUTPUT_I2C]: self.outputs[OUTPUT_I2C] = I2CInterface(self.util.meter_config, self.data_source) + + if self.util.meter_config[OUTPUT_PWM]: + self.outputs[OUTPUT_PWM] = PWMInterface(self.util.meter_config, self.data_source) self.start_interface_outputs() @@ -127,14 +128,17 @@ def start_interface_outputs(self): if self.util.meter_config[OUTPUT_I2C]: self.i2c_interface = self.outputs[OUTPUT_I2C] self.i2c_interface.start_writing() + + if self.util.meter_config[OUTPUT_PWM]: + self.pwm_interface = self.outputs[OUTPUT_PWM] + self.pwm_interface.start_writing() def start(self): """ Start VU meter. This method called by Peppy Meter to start meter """ pygame.event.clear() - if self.util.meter_config[DATA_SOURCE][TYPE] != SOURCE_PIPE or self.use_vu_meter == False: + if self.util.meter_config[DATA_SOURCE][TYPE] != SOURCE_PIPE: self.data_source.start_data_source() - logging.debug("started datasource") self.meter.start() def start_display_output(self): @@ -156,9 +160,8 @@ def start_display_output(self): def stop(self): """ Stop meter animation. """ - if self.util.meter_config[DATA_SOURCE][TYPE] != SOURCE_PIPE or self.use_vu_meter == False: + if self.util.meter_config[DATA_SOURCE][TYPE] != SOURCE_PIPE: self.data_source.stop_data_source() - logging.debug("stopped datasource") self.meter.stop() def refresh(self): @@ -166,6 +169,13 @@ def refresh(self): self.meter.refresh() + def set_volume(self, volume): + """ Set volume level. + + :param volume: new volume level + """ + self.data_source.volume = volume + def exit(self): """ Exit program """ @@ -173,6 +183,8 @@ def exit(self): self.serial_interface.stop_writing() if self.util.meter_config[OUTPUT_I2C]: self.i2c_interface.stop_writing() + if self.util.meter_config[OUTPUT_PWM]: + self.pwm_interface.stop_writing() pygame.quit() os._exit(0) diff --git a/screensaver/peppymeter/pwminterface.py b/screensaver/peppymeter/pwminterface.py new file mode 100644 index 00000000..92c13b02 --- /dev/null +++ b/screensaver/peppymeter/pwminterface.py @@ -0,0 +1,116 @@ +# Copyright 2019 PeppyMeter peppy.player@gmail.com +# +# This file is part of PeppyMeter. +# +# PeppyMeter is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PeppyMeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PeppyMeter. If not, see . + +import time +import sys +import logging + +from threading import Thread +from configfileparser import PWM_INTERFACE, FREQUENCY, GPIO_PIN_LEFT, GPIO_PIN_RIGHT, UPDATE_PERIOD + +class DummyPWM(object): + """ Dummy PWM class used for development on Windows platform """ + + def __init__(self): + """ Initializer """ + + pass + + def start(self, value): + """ Dummy start """ + + pass + + def ChangeDutyCycle(self, value): + """ Dummy change duty cycle """ + + pass + + def stop(self, value): + """ Dummy stop """ + + pass + +class PWMInterface(object): + """ PWM interface class. + + Can be used with devices controlled by means of PWM signal e.g. LED, gas tubes etc. + """ + def __init__(self, config, data_source): + """ Initializer """ + + self.data_source = data_source + + self.frequency = config[PWM_INTERFACE][FREQUENCY] + self.gpio_pin_left = config[PWM_INTERFACE][GPIO_PIN_LEFT] + self.gpio_pin_right = config[PWM_INTERFACE][GPIO_PIN_RIGHT] + self.update_period = config[PWM_INTERFACE][UPDATE_PERIOD] + + if "win" in sys.platform: + self.left = DummyPWM() + self.right = DummyPWM() + else: + import RPi.GPIO as gpio + gpio.setmode(gpio.BCM) + gpio.setwarnings(False) + + gpio.setup(self.gpio_pin_left, gpio.OUT) + gpio.setup(self.gpio_pin_right, gpio.OUT) + + self.left = gpio.PWM(self.gpio_pin_left, self.frequency) + self.right = gpio.PWM(self.gpio_pin_right, self.frequency) + + self.logging_template = "PWM left: {0} right: {1}" + + def start_writing(self): + """ Start writing thread """ + + self.running = True + thread = Thread(target = self.write_data) + thread.start() + + def write_data(self): + """ Method of the writing thread """ + + self.left.start(0) + self.right.start(0) + + while self.running: + time.sleep(self.update_period) + + v = self.data_source.get_value() + + if v == 0: + continue + + logging.debug(v) + left = float(int(v[0])) + right = float(int(v[1])) + + logging.debug(self.logging_template.format(left, right)) + + self.left.ChangeDutyCycle(left) + self.right.ChangeDutyCycle(right) + + def stop_writing(self): + """ Stop writing thread and stop PWM """ + + self.running = False + time.sleep(self.update_period) + self.left.stop() + self.right.stop() + \ No newline at end of file diff --git a/screensaver/peppymeter/serialinterface.py b/screensaver/peppymeter/serialinterface.py index 8cb77652..5e9f8bcd 100644 --- a/screensaver/peppymeter/serialinterface.py +++ b/screensaver/peppymeter/serialinterface.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 PeppyMeter peppy.player@gmail.com +# Copyright 2016-2019 PeppyMeter peppy.player@gmail.com # # This file is part of PeppyMeter. # @@ -63,7 +63,6 @@ def __init__(self, config, data_source): self.include_time = config[SERIAL_INTERFACE][INCLUDE_TIME] self.update_period = config[SERIAL_INTERFACE][UPDATE_PERIOD] self.serial_interface.open() - logging.debug("serial interface opened") def start_writing(self): """ Start writing thread """ @@ -78,14 +77,14 @@ def write_data(self): while self.running: v = self.data_source.get_value() data = self.get_data(v[0], v[1]) - logging.debug("serial output: " + data.rstrip()) + logging.debug("Serial output: " + data.rstrip()) self.serial_interface.write(data.encode("utf-8")) # self.serial_interface.readline() time.sleep(self.update_period) def get_data(self, left, right): - """ Prepareata for writing. Include time if enabled. + """ Prepare data for writing. Include time if enabled. :left: data for left channel :right: data for right channel diff --git a/screensaver/peppyweather/forecast.py b/screensaver/peppyweather/forecast.py index f9d53bba..71627fe2 100644 --- a/screensaver/peppyweather/forecast.py +++ b/screensaver/peppyweather/forecast.py @@ -173,7 +173,7 @@ def draw_temp(self, x, y, w, h, fcast): font_size = int((bb_h / 100) * 50) front_color = self.util.weather_config[COLOR_CONTRAST] - c = self.util.get_text_component(fcast[HIGH] + self.degree, front_color, font_size) + c = self.util.get_text_component(str(fcast[HIGH]) + self.degree, front_color, font_size) c.content_x = x + w - c.content.get_size()[0] c.content_y = bb_y + int((bb_h - c.content.get_size()[1]) / 2) + 6 diff --git a/screensaver/peppyweather/peppyweather.py b/screensaver/peppyweather/peppyweather.py index 46cb877a..9c4e9b02 100644 --- a/screensaver/peppyweather/peppyweather.py +++ b/screensaver/peppyweather/peppyweather.py @@ -47,14 +47,7 @@ def __init__(self, util=None): """ ScreensaverWeather.__init__(self) self.config = None - if util: - self.set_util(util) - else: - self.util = WeatherUtil() - base_path = "." - parser = WeatherConfigParser(base_path) - self.util.weather_config = parser.weather_config - + self.set_util(util) self.update_period = self.util.weather_config[UPDATE_PERIOD] if self.util.weather_config[USE_LOGGING]: @@ -82,7 +75,7 @@ def set_util(self, util): :param util: external utility object """ self.config = util.config - self.util = WeatherUtil() + self.util = WeatherUtil(util.k3, util.k4, util.k5) self.util.weather_config = util.weather_config path = os.path.join(os.getcwd(), SCREENSAVER, WEATHER) self.util.weather_config[BASE_PATH] = path diff --git a/screensaver/peppyweather/today.py b/screensaver/peppyweather/today.py index 391a5b1b..9b09e6af 100644 --- a/screensaver/peppyweather/today.py +++ b/screensaver/peppyweather/today.py @@ -25,7 +25,7 @@ COLOR_CONTRAST, COLOR_MEDIUM, COLOR_BRIGHT, COLOR_DARK_LIGHT, DEGREE, UNIT, UNKNOWN, HUMIDITY, WIND, \ SUNRISE, SUNSET, CITY_LABEL, MPH from weatherutil import CHILL, DIRECTION, SPEED, TEMPERATURE, VISIBILITY, HUMIDITY, PRESSURE, \ - SUNRISE, SUNSET, TEMP, TEXT, CODE, DATE, HIGH, LOW, CODE_UNKNOWN, ICONS_FOLDER, BLACK + SUNRISE, SUNSET, TEXT, CODE, DATE, HIGH, LOW, CODE_UNKNOWN, ICONS_FOLDER, BLACK TOP_HEIGHT = 15 BOTTOM_HEIGHT = 25 @@ -73,11 +73,11 @@ def set_weather(self, weather): self.chill = self.util.get_wind()[CHILL] self.direction = self.util.get_wind()[DIRECTION] - self.speed = self.util.get_wind()[SPEED] + self.speed = str(self.util.get_wind()[SPEED]) - self.visibility = self.util.get_atmosphere()[VISIBILITY] - self.humidity = self.util.get_atmosphere()[HUMIDITY] - self.pressure = self.util.get_atmosphere()[PRESSURE] + self.visibility = str(self.util.get_atmosphere()[VISIBILITY]) + self.humidity = str(self.util.get_atmosphere()[HUMIDITY]) + self.pressure = str(self.util.get_atmosphere()[PRESSURE]) a = self.util.get_astronomy() t = self.util.get_astronomy()[SUNRISE] @@ -85,21 +85,22 @@ def set_weather(self, weather): t = self.util.get_astronomy()[SUNSET] self.sunset = self.util.get_time(t) - self.temp = self.util.get_condition()[TEMP] + self.temp = str(self.util.get_condition()[TEMPERATURE]) self.mph = self.weather_config[MPH] - self.temp_unit = self.util.get_units()[TEMPERATURE] + self.temp_unit = self.util.get_units() - self.code = self.util.get_condition()[CODE] + self.code = str(self.util.get_condition()[CODE]) self.txt = self.weather_config[self.code] self.code_image = self.util.code_image_map[int(self.code)] - d = self.util.get_condition()[DATE] + ms = int(self.util.current_observation[DATE]) + d = datetime.fromtimestamp(ms) self.time = self.util.get_time_from_date(d) today_weather = self.util.get_forecast()[0] - self.high = today_weather[HIGH] - self.low = today_weather[LOW] + self.high = str(today_weather[HIGH]) + self.low = str(today_weather[LOW]) def set_unknown_weather(self): """ Set parameters in case of unavailable weather """ diff --git a/screensaver/peppyweather/weatherutil.py b/screensaver/peppyweather/weatherutil.py index 65e88a55..47a00a73 100644 --- a/screensaver/peppyweather/weatherutil.py +++ b/screensaver/peppyweather/weatherutil.py @@ -20,23 +20,23 @@ import time import json import logging +import base64 +import oauth2 as oauth +import requests +from random import randint from component import Component -from urllib import request from weatherconfigparser import CITY, REGION, COUNTRY, UNIT, BASE_PATH, \ MILITARY_TIME_FORMAT from svg import Parser, Rasterizer -QUERY = "query" -RESULTS = "results" -CHANNEL = "channel" +CURRENT_OBSERVATION = "current_observation" LOCATION = "location" WIND = "wind" ATMOSPHERE = "atmosphere" ASTRONOMY = "astronomy" -ITEM = "item" CONDITION = "condition" -FORECAST = "forecast" +FORECASTS = "forecasts" CHILL = "chill" DIRECTION = "direction" SPEED = "speed" @@ -47,8 +47,7 @@ SUNRISE = "sunrise" SUNSET = "sunset" CODE = "code" -DATE = "date" -TEMP = "temp" +DATE = "pubDate" TEXT = "text" DAY = "day" HIGH = "high" @@ -61,9 +60,14 @@ class WeatherUtil(object): """ Utility class """ - def __init__(self): - """ Initializer """ - + def __init__(self, app_id, app_key, app_sec): + """ Initializer + + :param util: utility object + """ + self.app_id = app_id + self.app_key = app_key + self.app_sec = app_sec self.image_cache = {} self.code_image_map = {} self.code_image_map[0] = "tornado.svg" @@ -129,26 +133,39 @@ def set_url(self): country = weather_config[COUNTRY].lstrip().rstrip() unit = weather_config[UNIT].lstrip().rstrip() - weather_url_prefix = "https://query.yahooapis.com/v1/public/yql?q=select* from weather.forecast where woeid in (select woeid from geo.places(1) where text='" - weather_url_unit = "') and u='" + weather_url_prefix = "https://weather-ydn-yql.media.yahoo.com/forecastrss?location=" + weather_url_unit = "&u=" weather_url_suffix = "'&format=json" - self.url = weather_url_prefix + city + "," + region + country + weather_url_unit + unit + weather_url_suffix + self.url = weather_url_prefix + city + "," + region + country + "&u=" + unit + "&format=json" self.url.encode('ascii') self.url = self.url.replace(" ", "%20") - + def load_json(self): """ Load weather json object from Yahoo Weather """ - logging.debug("request: " + self.url) - req = request.Request(self.url) + header = { + "Yahoo-App-Id": self.app_id, + "Authorization": "OAuth", + "oauth_consumer_key": self.app_key, + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": str(int(time.time())), + "oauth_nonce": oauth.generate_nonce(), + "oauth_version": "1.0" + } + + req = oauth.Request(method="GET", url=self.url, parameters=header) + consumer = oauth.Consumer(key=self.app_key, secret=self.app_sec) + signature = oauth.SignatureMethod_HMAC_SHA1().sign(req, consumer, None) + req["oauth_signature"] = signature + + logging.debug("request url: " + self.url) response = None html = None try: - site = request.urlopen(req) - charset = site.info().get_content_charset() - html = site.read() + site = requests.get(req.to_url()) + html = site.content except: pass @@ -157,22 +174,17 @@ def load_json(self): return None try: - response = html.decode(charset) + response = html.decode("utf-8") except: pass - logging.debug("response: " + response) + self.weather = self.current_observation = self.forecasts = None + self.weather = json.loads(response) - self.weather = None - self.weather = json.loads(response) - - if self.weather and self.weather[QUERY] and self.weather[QUERY][RESULTS]: - self.channel = self.weather[QUERY][RESULTS][CHANNEL] - - self.item = None - try: - self.item = self.channel[ITEM] - except: + if self.weather and self.weather[CURRENT_OBSERVATION] and self.weather[FORECASTS]: + self.current_observation = self.weather[CURRENT_OBSERVATION] + self.forecasts = self.weather[FORECASTS] + else: self.weather = None return self.weather @@ -182,49 +194,49 @@ def get_units(self): :return: units """ - return self.channel[UNITS] + return self.weather_config[UNIT].upper() def get_location(self): """ Get location section :return: location section """ - return self.channel[LOCATION] + return self.weather[LOCATION] def get_wind(self): """ Get wind section :return: wind section """ - return self.channel[WIND] + return self.current_observation[WIND] def get_atmosphere(self): """ Get atmosphere section :return: atmosphere section """ - return self.channel[ATMOSPHERE] + return self.current_observation[ATMOSPHERE] def get_astronomy(self): """ Get astronomy section (sunrise/sunset) :return: astronomy section """ - return self.channel[ASTRONOMY] + return self.current_observation[ASTRONOMY] def get_condition(self): """ Get condition section :return: condition section """ - return self.item[CONDITION] + return self.current_observation[CONDITION] def get_forecast(self): """ Get forecast section :return: forecast section """ - return self.item[FORECAST] + return self.forecasts def load_svg_icon(self, folder, image_name, bounding_box=None): """ Load SVG image @@ -330,9 +342,7 @@ def get_time_from_date(self, d): else: self.TIME_FORMAT = "%I:%M %p" - d = d[0 : d.rfind(" ")] - current_time = time.strptime(d, '%a, %d %b %Y %I:%M %p') - return time.strftime(self.TIME_FORMAT, current_time) + return d.strftime(self.TIME_FORMAT) def get_time(self, t): """ Get time diff --git a/screensaver/screensaverdispatcher.py b/screensaver/screensaverdispatcher.py index ba83f6fc..c349e816 100644 --- a/screensaver/screensaverdispatcher.py +++ b/screensaver/screensaverdispatcher.py @@ -20,7 +20,7 @@ from ui.component import Component from util.keys import USER_EVENT_TYPE from util.config import SCREEN_INFO, FRAME_RATE, SCREENSAVER, NAME, DELAY, \ - KEY_SCREENSAVER_DELAY_1, KEY_SCREENSAVER_DELAY_3 + KEY_SCREENSAVER_DELAY_1, KEY_SCREENSAVER_DELAY_3, USAGE, USE_VU_METER, VUMETER DELAY_1 = 60 DELAY_3 = 180 @@ -164,6 +164,13 @@ def change_volume(self, volume): """ self.current_volume = volume.position self.current_screensaver.set_volume(self.current_volume) + + if self.config[USAGE][USE_VU_METER]: + try: + vu_meter = self.util.screensaver_cache[VUMETER] + vu_meter.set_volume(self.current_volume) + except: + pass def handle_event(self, event): """ Handle user input events. Stop screensaver if it's running or restart dispatcher. diff --git a/screensaver/slideshow/slides/auto.png b/screensaver/slideshow/slides/auto.png new file mode 100644 index 00000000..130adefc Binary files /dev/null and b/screensaver/slideshow/slides/auto.png differ diff --git a/screensaver/slideshow/slides/four.png b/screensaver/slideshow/slides/four.png new file mode 100644 index 00000000..1e7ca55e Binary files /dev/null and b/screensaver/slideshow/slides/four.png differ diff --git a/screensaver/slideshow/slides/lady.png b/screensaver/slideshow/slides/lady.png deleted file mode 100644 index 3d7b9b1a..00000000 Binary files a/screensaver/slideshow/slides/lady.png and /dev/null differ diff --git a/screensaver/slideshow/slides/rhino.png b/screensaver/slideshow/slides/rhino.png new file mode 100644 index 00000000..2d0b0817 Binary files /dev/null and b/screensaver/slideshow/slides/rhino.png differ diff --git a/screensaver/slideshow/slides/son.png b/screensaver/slideshow/slides/son.png deleted file mode 100644 index 3a6f3333..00000000 Binary files a/screensaver/slideshow/slides/son.png and /dev/null differ diff --git a/screensaver/slideshow/slides/toledo.png b/screensaver/slideshow/slides/toledo.png deleted file mode 100644 index 2e028339..00000000 Binary files a/screensaver/slideshow/slides/toledo.png and /dev/null differ diff --git a/scripts/shutdown.py b/scripts/shutdown.py new file mode 100644 index 00000000..8f5afcd5 --- /dev/null +++ b/scripts/shutdown.py @@ -0,0 +1,35 @@ +# Copyright 2019 PeppyMeter peppy.player@gmail.com +# +# This file is part of PeppyMeter. +# +# PeppyMeter is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PeppyMeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PeppyMeter. If not, see . + +import sys +import logging + +from threading import Thread + +class Shutdown(object): + """ Example of the synchronous shutdown script """ + + def __init__(self): + """ Initializer """ + + self.type = "sync" + logging.debug("Shutdown script initializer called") + + def start(self): + """ Shutdown logic goes here """ + + logging.debug("Running synchronous shutdown script") diff --git a/scripts/startup.py b/scripts/startup.py new file mode 100644 index 00000000..1d2eb600 --- /dev/null +++ b/scripts/startup.py @@ -0,0 +1,42 @@ +# Copyright 2019 PeppyMeter peppy.player@gmail.com +# +# This file is part of PeppyMeter. +# +# PeppyMeter is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PeppyMeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PeppyMeter. If not, see . + +import sys +import logging + +from threading import Thread + +class Startup(object): + """ Example of the asynchronous startup script. """ + + def __init__(self): + """ Initializer """ + + self.type = "async" + logging.debug("Startup script initializer called") + + def start_thread(self): + """ Start thread """ + + self.running = True + thread = Thread(target = self.run_async_thread) + thread.start() + + def run_async_thread(self): + """ Async thread method """ + + logging.debug("Started startup asynchronous thread") diff --git a/splash-800.png b/splash-800.png index 3b1cdd07..f7f77189 100644 Binary files a/splash-800.png and b/splash-800.png differ diff --git a/splash.png b/splash.png index ce4a9394..6e17dc09 100644 Binary files a/splash.png and b/splash.png differ diff --git a/storage b/storage index 89be7b58..dd32ddf4 100644 Binary files a/storage and b/storage differ diff --git a/ui/button/episodebutton.py b/ui/button/episodebutton.py new file mode 100644 index 00000000..7d1243fe --- /dev/null +++ b/ui/button/episodebutton.py @@ -0,0 +1,135 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import textwrap + +from ui.component import Component +from ui.button.button import Button +from util.keys import MAXIMUM_FONT_SIZE, V_ALIGN, V_ALIGN_TOP, V_OFFSET, H_ALIGN, H_ALIGN_LEFT +from util.config import SCREEN_INFO, WIDTH + +class EpisodeButton(Button): + """ Podcast episode button """ + + def __init__(self, util, state): + """ Initializer + + :param util: utility object + :param state: button state + """ + Button.__init__(self, util, state) + + def add_label(self, state, bb): + """ Add button label + + :param state: button state + :param bb: bounding box + """ + if not self.show_label: + self.add_component(None) + return + + fixed_height = getattr(state, "fixed_height", None) + if fixed_height: + font_size = fixed_height + else: + font_size = int((bb.h * state.label_text_height)/100.0) + + if font_size > self.config[MAXIMUM_FONT_SIZE]: + font_size = self.config[MAXIMUM_FONT_SIZE] + + font = self.util.get_font(font_size) + text = self.truncate_long_labels(state.l_name, bb, font) + state.l_name = text + size = font.size(text) + label = font.render(text, 1, state.text_color_normal) + c = Component(self.util, label) + c.name = state.name + ".label" + c.text = text + c.text_size = font_size + c.text_color_normal = state.text_color_normal + c.text_color_selected = state.text_color_selected + c.text_color_disabled = state.text_color_disabled + c.text_color_current = c.text_color_normal + + h_align = getattr(state, H_ALIGN, None) + if h_align != None: + if h_align == H_ALIGN_LEFT: + c.content_x = bb.x + else: + c.content_x = bb.x + (bb.width - size[0])/2 + + v_align = getattr(state, V_ALIGN, None) + if v_align and v_align == V_ALIGN_TOP: + v_offset = getattr(state, V_OFFSET, 0) + if v_offset != 0: + v_offset = int((bb.height / 100) * v_offset) + c.content_y = bb.y + v_offset + else: + c.content_y = bb.y + else: + c.content_y = bb.y + (bb.height - size[1])/2 + + if len(self.components) == 2: + self.components.append(c) + else: + self.components[2] = c + + desc = getattr(state, "description", None) + if desc != None: + self.add_description(state, desc, c.content_y, size[1], bb, font_size) + + def add_description(self, state, desc, title_y, title_h, bb, font_size): + """ Add episode description + + :param state: button state + :param desc: description text + :param title_y: y coordinate + :param title_h: text height + :param bb: bounding box + :param font_size: + """ + if self.config[SCREEN_INFO][WIDTH] <= 320: + desc_font_size = int(font_size * 0.8) + line_length = 56 + elif self.config[SCREEN_INFO][WIDTH] > 320 and self.config[SCREEN_INFO][WIDTH] <= 480: + desc_font_size = int(font_size * 0.7) + line_length = 70 + else: + desc_font_size = int(font_size * 0.7) + line_length = 74 + + lines = textwrap.wrap(desc, line_length) + font = self.util.get_font(desc_font_size) + + for n, line in enumerate(lines[0:3]): + try: + label = font.render(line, 1, state.text_color_normal) + except: + continue + c = Component(self.util, label) + c.name = "desc." + str(title_y) + str(n) + c.text = line + c.text_size = desc_font_size + c.text_color_normal = state.text_color_normal + c.text_color_selected = state.text_color_selected + c.text_color_disabled = state.text_color_disabled + c.text_color_current = c.text_color_normal + c.content_x = bb.x + c.content_y = title_y + (title_h * 0.8) + (n * desc_font_size) + self.components.append(c) + \ No newline at end of file diff --git a/ui/button/podcastbutton.py b/ui/button/podcastbutton.py new file mode 100644 index 00000000..283767df --- /dev/null +++ b/ui/button/podcastbutton.py @@ -0,0 +1,129 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import textwrap + +from ui.component import Component +from ui.button.button import Button +from util.keys import MAXIMUM_FONT_SIZE, V_ALIGN, V_ALIGN_TOP, V_OFFSET, H_ALIGN, H_ALIGN_LEFT +from util.config import SCREEN_INFO, WIDTH + +class PodcastButton(Button): + """ Podcast button """ + + def __init__(self, util, state): + """ Initializer + + :param util: utility object + :param state: button state + """ + Button.__init__(self, util, state) + + def add_label(self, state, bb): + """ Add button label + + :param state: button state + :param bb: bounding box + """ + if not self.show_label: + self.add_component(None) + return + + fixed_height = getattr(state, "fixed_height", None) + if fixed_height: + font_size = fixed_height + else: + font_size = int((bb.h * state.label_text_height)/100.0) + + if font_size > self.config[MAXIMUM_FONT_SIZE]: + font_size = self.config[MAXIMUM_FONT_SIZE] + + font = self.util.get_font(font_size) + text = self.truncate_long_labels(state.l_name, bb, font) + state.l_name = text + size = font.size(text) + label = font.render(text, 1, state.text_color_normal) + c = Component(self.util, label) + c.name = state.name + ".label" + c.text = text + c.text_size = font_size + c.text_color_normal = state.text_color_normal + c.text_color_selected = state.text_color_selected + c.text_color_disabled = state.text_color_disabled + c.text_color_current = c.text_color_normal + + h_align = getattr(state, H_ALIGN, None) + if h_align != None: + if h_align == H_ALIGN_LEFT: + c.content_x = bb.x + else: + c.content_x = bb.x + (bb.width - size[0])/2 + + v_align = getattr(state, V_ALIGN, None) + if v_align and v_align == V_ALIGN_TOP: + v_offset = getattr(state, V_OFFSET, 0) + if v_offset != 0: + v_offset = int((bb.height / 100) * v_offset) + c.content_y = bb.y + v_offset + else: + c.content_y = bb.y + else: + c.content_y = bb.y + (bb.height - size[1])/2 + 1 + + if len(self.components) == 2: + self.components.append(c) + else: + self.components[2] = c + + desc = getattr(state, "description", None) + if desc != None: + self.add_description(state, desc, c.content_y, size[1], bb, font_size) + + def add_description(self, state, desc, title_y, title_h, bb, font_size): + """ Add podcast description + + :param state: button state + :param desc: description text + :param title_y: y coordinate + :param title_h: text height + :param bb: bounding box + :param font_size: + """ + desc_font_size = int(font_size * 0.7) + font = self.util.get_font(desc_font_size) + if self.config[SCREEN_INFO][WIDTH] <= 320: + line_length = 56 + elif self.config[SCREEN_INFO][WIDTH] > 320 and self.config[SCREEN_INFO][WIDTH] <= 480: + line_length = 58 + else: + line_length = 64 + lines = textwrap.wrap(desc, line_length) + + for n, line in enumerate(lines[0:5]): + label = font.render(line, 1, state.text_color_normal) + c = Component(self.util, label) + c.name = "desc." + str(title_y) + str(n) + c.text = line + c.text_size = desc_font_size + c.text_color_normal = state.text_color_normal + c.text_color_selected = state.text_color_selected + c.text_color_disabled = state.text_color_disabled + c.text_color_current = c.text_color_normal + c.content_x = bb.x + c.content_y = title_y + title_h + (n * desc_font_size) + self.components.append(c) + \ No newline at end of file diff --git a/ui/factory.py b/ui/factory.py index 940460f5..848842a0 100644 --- a/ui/factory.py +++ b/ui/factory.py @@ -17,6 +17,8 @@ from pygame import Rect from ui.button.button import Button +from ui.button.podcastbutton import PodcastButton +from ui.button.episodebutton import EpisodeButton from ui.state import State from ui.button.multistatebutton import MultiStateButton from ui.button.togglebutton import ToggleButton @@ -28,7 +30,7 @@ from ui.layout.buttonlayout import BOTTOM, CENTER, LEFT, RIGHT from util.keys import kbd_keys, KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_PLAY_PAUSE, KEY_MENU, \ KEY_END, KEY_MUTE, KEY_SELECT, KEY_LEFT, KEY_RIGHT, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SETUP, \ - TRACK_MENU, BOOK_MENU, HOME_NAVIGATOR + TRACK_MENU, BOOK_MENU, HOME_NAVIGATOR, V_ALIGN_TOP, H_ALIGN_LEFT, FILE_BUTTON from util.util import IMAGE_SELECTED_SUFFIX, IMAGE_VOLUME, IMAGE_MUTE, V_ALIGN_CENTER, V_ALIGN_BOTTOM, \ H_ALIGN_CENTER, IMAGE_TIME_KNOB, KEY_HOME, KEY_PLAYER from util.config import COLOR_DARK, COLOR_DARK_LIGHT, COLOR_MEDIUM, COLORS, COLOR_CONTRAST, COLOR_BRIGHT, \ @@ -331,6 +333,85 @@ def create_dynamic_text(self, name, bb, bgr, fgr, font_size, halign=H_ALIGN_CENT dynamicText = DynamicText(**d) return dynamicText + def create_podcast_menu_button(self, s, constr, action, scale): + """ Create podcast menu button + + :param s: button state + :param constr: scaling constraints + :param action: button event listener + :param scale: True - scale images, False - don't scale images + + :return: genre menu button + """ + s.bounding_box = constr + s.img_x = None + s.img_y = None + s.auto_update = True + s.show_bgr = True + s.show_img = True + s.show_label = True + s.image_location = LEFT + s.label_location = CENTER + s.label_area_percent = 30 + s.image_size_percent = 0.25 + s.text_color_normal = self.config[COLORS][COLOR_BRIGHT] + s.text_color_selected = self.config[COLORS][COLOR_CONTRAST] + s.text_color_disabled = self.config[COLORS][COLOR_MEDIUM] + s.text_color_current = s.text_color_normal + s.scale = scale + s.source = None + s.v_align = V_ALIGN_TOP + s.h_align = H_ALIGN_LEFT + s.v_offset = (constr.h/100) * 5 + button = PodcastButton(self.util, s) + button.add_release_listener(action) + if not getattr(s, "enabled", True): + button.set_enabled(False) + elif getattr(s, "icon_base", False) and not getattr(s, "scaled", False): + button.components[1].content = s.icon_base + button.scaled = scale + return button + + def create_episode_menu_button(self, s, constr, action, scale): + """ Create podcast episode menu button + + :param s: button state + :param constr: scaling constraints + :param action: button event listener + :param scale: True - scale images, False - don't scale images + + :return: genre menu button + """ + s.bounding_box = constr + s.img_x = None + s.img_y = None + s.auto_update = True + s.show_bgr = True + s.show_img = True + s.show_label = True + s.image_location = LEFT + s.label_location = CENTER + s.label_area_percent = 30 + s.image_size_percent = 0.12 + s.text_color_normal = self.config[COLORS][COLOR_BRIGHT] + s.text_color_selected = self.config[COLORS][COLOR_CONTRAST] + s.text_color_disabled = self.config[COLORS][COLOR_MEDIUM] + s.text_color_current = s.text_color_normal + s.scale = scale + s.source = "episode_menu" + s.v_align = V_ALIGN_TOP + s.h_align = H_ALIGN_LEFT + s.v_offset = (constr.h/100) * 5 + button = EpisodeButton(self.util, s) + button.add_release_listener(action) + if not getattr(s, "enabled", True): + button.set_enabled(False) + elif getattr(s, "icon_base", False) and not getattr(s, "scaled", False): + button.components[1].content = s.icon_base + button.scaled = scale + return button + + def create_menu_button(self, s, constr, action, scale, label_area_percent=30, label_text_height=44, show_img=True, show_label=True, bgr=None, source=None, font_size=None): """ Create Menu button @@ -953,7 +1034,10 @@ def create_file_menu_button(self, s, constr, action, scale): scale = False if s.file_type == FOLDER_WITH_ICON: scale = True - return self.create_menu_button(s, constr, action, scale, label_text_height=80) + if hasattr(s, "show_label"): + return self.create_menu_button(s, constr, action, scale, label_text_height=80, show_label=s.show_label) + else: + return self.create_menu_button(s, constr, action, scale, label_text_height=80) def create_file_button(self, bb, action=None): """ Create default audio file button @@ -976,6 +1060,7 @@ def create_file_button(self, bb, action=None): state.show_bgr = False state.show_img = True state.image_align_v = V_ALIGN_CENTER + state.source = FILE_BUTTON button = Button(self.util, state) button.add_release_listener(action) return button diff --git a/ui/menu/episodenavigator.py b/ui/menu/episodenavigator.py new file mode 100644 index 00000000..28e292db --- /dev/null +++ b/ui/menu/episodenavigator.py @@ -0,0 +1,98 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +from ui.container import Container +from ui.layout.gridlayout import GridLayout +from ui.layout.borderlayout import BorderLayout +from ui.factory import Factory +from util.keys import GO_LEFT_PAGE, GO_RIGHT_PAGE, KEY_HOME, KEY_PLAYER, \ + KEY_PLAY_PAUSE, KEY_BACK, GO_BACK, KEY_PODCASTS_MENU, KEY_PARENT +from util.config import PODCASTS + +PERCENT_ARROW_WIDTH = 16.0 + +class EpisodeNavigator(Container): + """ Episodes screen navigator menu """ + + def __init__(self, util, bounding_box, listeners, bgr, pages): + """ Initializer + + :param util: utility object + :param bounding_box: bounding box + :param listeners: buttons listeners + :param bgr: menu background + :param pages: number of episodes menu pages + """ + Container.__init__(self, util) + self.factory = Factory(util) + self.name = "podcasts.navigator" + self.content = bounding_box + self.content_x = bounding_box.x + self.content_y = bounding_box.y + self.menu_buttons = [] + + arrow_layout = BorderLayout(bounding_box) + arrow_layout.set_percent_constraints(0, 0, PERCENT_ARROW_WIDTH, PERCENT_ARROW_WIDTH) + + if pages > 1: + constr = arrow_layout.LEFT + self.left_button = self.factory.create_page_down_button(constr, "0", 40, 100) + self.left_button.add_release_listener(listeners[GO_LEFT_PAGE]) + self.add_component(self.left_button) + self.menu_buttons.append(self.left_button) + + constr = arrow_layout.RIGHT + self.right_button = self.factory.create_page_up_button(constr, "0", 40, 100) + self.right_button.add_release_listener(listeners[GO_RIGHT_PAGE]) + self.add_component(self.right_button) + self.menu_buttons.append(self.right_button) + layout = GridLayout(arrow_layout.CENTER) + else: + layout = GridLayout(bounding_box) + + layout.set_pixel_constraints(1, 4, 1, 0) + layout.current_constraints = 0 + image_size = 64 + + constr = layout.get_next_constraints() + self.home_button = self.factory.create_button(KEY_HOME, KEY_HOME, constr, listeners[KEY_HOME], bgr, image_size_percent=image_size) + self.add_component(self.home_button) + self.menu_buttons.append(self.home_button) + + constr = layout.get_next_constraints() + self.back_button = self.factory.create_button(KEY_PODCASTS_MENU, KEY_PARENT, constr, listeners[PODCASTS], bgr, image_size_percent=image_size) + self.add_component(self.back_button) + self.menu_buttons.append(self.back_button) + + constr = layout.get_next_constraints() + self.back_button = self.factory.create_button(KEY_BACK, KEY_BACK, constr, listeners[GO_BACK], bgr, image_size_percent=image_size) + self.add_component(self.back_button) + self.menu_buttons.append(self.back_button) + + constr = layout.get_next_constraints() + self.player_button = self.factory.create_button(KEY_PLAYER, KEY_PLAY_PAUSE, constr, listeners[KEY_PLAYER], bgr, image_size_percent=image_size) + self.add_component(self.player_button) + self.menu_buttons.append(self.player_button) + + def add_observers(self, update_observer, redraw_observer): + """ Add screen observers + + :param update_observer: observer for updating the screen + :param redraw_observer: observer to redraw the whole screen + """ + for b in self.menu_buttons: + self.add_button_observers(b, update_observer, redraw_observer) diff --git a/ui/menu/homemenu.py b/ui/menu/homemenu.py index b5f62008..55b3b693 100644 --- a/ui/menu/homemenu.py +++ b/ui/menu/homemenu.py @@ -21,7 +21,7 @@ from util.cdutil import CdUtil from util.keys import LINUX_PLATFORM, V_ALIGN_TOP from util.config import USAGE, USE_VOICE_ASSISTANT, HOME_MENU, RADIO, AUDIO_FILES, \ - CURRENT, MODE, NAME, AUDIOBOOKS, STREAM, CD_PLAYER, AUDIO, PLAYER_NAME + CURRENT, MODE, NAME, AUDIOBOOKS, STREAM, CD_PLAYER, PODCASTS, AUDIO, PLAYER_NAME class HomeMenu(Menu): """ Home Menu class. Extends base Menu class """ @@ -66,6 +66,15 @@ def __init__(self, util, bgr=None, bounding_box=None, font_size=None): if len(cd_drives_info) == 0: disabled_items.append(CD_PLAYER) items.append(CD_PLAYER) + + if self.config[HOME_MENU][PODCASTS]: + podcasts_util = util.get_podcasts_util() + podcasts = podcasts_util.get_podcasts_links() + downloads = podcasts_util.are_there_any_downloads() + connected = util.connected_to_internet + if (connected and len(podcasts) == 0 and not downloads) or (not connected and not downloads): + disabled_items.append(PODCASTS) + items.append(PODCASTS) l = self.get_layout(items) bounding_box = l.get_next_constraints() diff --git a/ui/menu/podcastnavigator.py b/ui/menu/podcastnavigator.py new file mode 100644 index 00000000..91f9c95a --- /dev/null +++ b/ui/menu/podcastnavigator.py @@ -0,0 +1,92 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +from ui.container import Container +from ui.layout.gridlayout import GridLayout +from ui.layout.borderlayout import BorderLayout +from ui.factory import Factory +from util.keys import GO_LEFT_PAGE, GO_RIGHT_PAGE, KEY_HOME, KEY_PLAYER, \ + KEY_PLAY_PAUSE, KEY_BACK, GO_BACK + +PERCENT_ARROW_WIDTH = 16.0 + +class PodcastNavigator(Container): + """ Podcasts screen navigator menu """ + + def __init__(self, util, bounding_box, listeners, bgr, pages): + """ Initializer + + :param util: utility object + :param bounding_box: bounding box + :param listeners: buttons listeners + :param bgr: menu background + :param pages: number of podcasts menu pages + """ + Container.__init__(self, util) + self.factory = Factory(util) + self.name = "podcasts.navigator" + self.content = bounding_box + self.content_x = bounding_box.x + self.content_y = bounding_box.y + self.menu_buttons = [] + + arrow_layout = BorderLayout(bounding_box) + arrow_layout.set_percent_constraints(0, 0, PERCENT_ARROW_WIDTH, PERCENT_ARROW_WIDTH) + + if pages > 1: + constr = arrow_layout.LEFT + self.left_button = self.factory.create_page_down_button(constr, "0", 40, 100) + self.left_button.add_release_listener(listeners[GO_LEFT_PAGE]) + self.add_component(self.left_button) + self.menu_buttons.append(self.left_button) + + constr = arrow_layout.RIGHT + self.right_button = self.factory.create_page_up_button(constr, "0", 40, 100) + self.right_button.add_release_listener(listeners[GO_RIGHT_PAGE]) + self.add_component(self.right_button) + self.menu_buttons.append(self.right_button) + layout = GridLayout(arrow_layout.CENTER) + else: + layout = GridLayout(bounding_box) + + layout.set_pixel_constraints(1, 3, 1, 0) + layout.current_constraints = 0 + image_size = 64 + + constr = layout.get_next_constraints() + self.home_button = self.factory.create_button(KEY_HOME, KEY_HOME, constr, listeners[KEY_HOME], bgr, image_size_percent=image_size) + self.add_component(self.home_button) + self.menu_buttons.append(self.home_button) + + constr = layout.get_next_constraints() + self.back_button = self.factory.create_button(KEY_BACK, KEY_BACK, constr, listeners[GO_BACK], bgr, image_size_percent=image_size) + self.add_component(self.back_button) + self.menu_buttons.append(self.back_button) + + constr = layout.get_next_constraints() + self.player_button = self.factory.create_button(KEY_PLAYER, KEY_PLAY_PAUSE, constr, listeners[KEY_PLAYER], bgr, image_size_percent=image_size) + self.add_component(self.player_button) + self.menu_buttons.append(self.player_button) + + def add_observers(self, update_observer, redraw_observer): + """ Add screen observers + + :param update_observer: observer for updating the screen + :param redraw_observer: observer to redraw the whole screen + """ + for b in self.menu_buttons: + self.add_button_observers(b, update_observer, redraw_observer) diff --git a/ui/page.py b/ui/page.py index 3e3f0fb3..e62d03f0 100644 --- a/ui/page.py +++ b/ui/page.py @@ -82,6 +82,22 @@ def set_current_item_by_url(self, url): if index: self.set_current_item(index) + + def set_current_item_by_file_name(self, file_name): + """ Set the current item by its file name + + :param file_name: item file name + """ + if self.items == None: return + + index = None + for item in self.items: + if item.file_name == file_name: + index = item.index + break + + if index: + self.set_current_item(index) def get_current_page(self): """ Get the current page for the current item diff --git a/ui/screen/about.py b/ui/screen/about.py index b619f124..1744d2cd 100644 --- a/ui/screen/about.py +++ b/ui/screen/about.py @@ -43,7 +43,7 @@ def __init__(self, util): self.bounding_box = self.config[SCREEN_RECT] self.start_listeners = [] factory = Factory(util) - edition = "El Greco Edition" + edition = "Durer Edition" layout = BorderLayout(self.bounding_box) layout.set_percent_constraints(0, PERCENT_FOOTER_HEIGHT, 0, 0) diff --git a/ui/screen/cddrives.py b/ui/screen/cddrives.py index 0ea8335b..0e6ef951 100644 --- a/ui/screen/cddrives.py +++ b/ui/screen/cddrives.py @@ -53,7 +53,7 @@ def __init__(self, util, listeners, voice_assistant): self.bounding_box = self.config[SCREEN_RECT] layout = BorderLayout(self.bounding_box) layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_BOTTOM_HEIGHT, 0, 0) - Screen.__init__(self, util, "", PERCENT_TOP_HEIGHT, voice_assistant, "file_browser_screen_title", True, layout.TOP) + Screen.__init__(self, util, "", PERCENT_TOP_HEIGHT, voice_assistant, "cd_drives_screen_title", True, layout.TOP) color_dark_light = self.config[COLORS][COLOR_DARK_LIGHT] self.cd_drive_id = self.config[CD_PLAYBACK][CD_DRIVE_ID] diff --git a/ui/screen/fileplayer.py b/ui/screen/fileplayer.py index d8581160..4f0ed5d3 100644 --- a/ui/screen/fileplayer.py +++ b/ui/screen/fileplayer.py @@ -56,7 +56,10 @@ def __init__(self, listeners, util, get_current_playlist, voice_assistant, playe """ Initializer :param listeners: screen listeners - :param util: utility object + :param util: utility object + :param get_current_playlist: current playlist getter + :param voice_assistant: voice assistant + :param player_stop: stop player function """ self.util = util self.config = util.config @@ -142,6 +145,8 @@ def get_current_track_index(self, state=None): In case of files goes through the file list. In case of playlist takes track index from the state object. + :param state: button state + :return: current track index """ if self.config[CURRENT][MODE] == AUDIOBOOKS: @@ -424,6 +429,7 @@ def set_current(self, new_track=False, state=None): """ Set current file or playlist :param new_track: True - new audio file + :param state: button state """ self.cd_album = getattr(state, "album", None) diff --git a/ui/screen/menuscreen.py b/ui/screen/menuscreen.py index 10faf28a..5b052ec4 100644 --- a/ui/screen/menuscreen.py +++ b/ui/screen/menuscreen.py @@ -19,8 +19,7 @@ from ui.layout.borderlayout import BorderLayout from ui.factory import Factory from ui.menu.booknavigator import BookNavigator -from util.keys import SCREEN_RECT, \ - GO_LEFT_PAGE, GO_RIGHT_PAGE, KEY_LOADING, LABELS +from util.keys import SCREEN_RECT, GO_LEFT_PAGE, GO_RIGHT_PAGE from util.cache import Cache from ui.layout.multilinebuttonlayout import MultiLineButtonLayout, LINES from util.config import COLOR_DARK, COLOR_BRIGHT, COLORS, COLOR_DARK_LIGHT, COLOR_CONTRAST @@ -44,7 +43,7 @@ class MenuScreen(Screen): """ Site Menu Screen. Base class for all book menu screens """ - def __init__(self, util, listeners, rows, columns, voice_assistant, d=None, turn_page=None, page_in_title=True): + def __init__(self, util, listeners, rows, columns, voice_assistant, d=None, turn_page=None, page_in_title=True, show_loading=False): """ Initializer :param util: utility object @@ -61,6 +60,7 @@ def __init__(self, util, listeners, rows, columns, voice_assistant, d=None, turn self.player = None self.turn_page = turn_page self.page_in_title = page_in_title + self.show_loading = show_loading self.cache = Cache(self.util) self.layout = BorderLayout(self.bounding_box) @@ -87,9 +87,6 @@ def __init__(self, util, listeners, rows, columns, voice_assistant, d=None, turn self.current_page = 1 self.menu = None - self.loading_listeners = [] - self.LOADING = util.config[LABELS][KEY_LOADING] - def get_menu_button_layout(self, d): """ Return menu button layout @@ -130,8 +127,15 @@ def previous_page(self, state): self.components[1].selected_index = 0 self.menu.current_page = self.current_page - self.menu.selected_index = 0 - self.turn_page() + self.menu.selected_index = 0 + + if self.show_loading: + self.set_loading(self.screen_title.text) + + self.turn_page() + + if self.show_loading: + self.reset_loading() def next_page(self, state): """ Handle click on right button @@ -148,7 +152,14 @@ def next_page(self, state): self.menu.current_page = self.current_page self.menu.selected_index = 0 - self.turn_page() + + if self.show_loading: + self.set_loading(self.screen_title.text) + + self.turn_page() + + if self.show_loading: + self.reset_loading() def set_menu(self, menu): """ Set menu @@ -232,35 +243,5 @@ def set_loading(self, name): :name: screen title """ - b = self.config[COLORS][COLOR_DARK] - f = self.config[COLORS][COLOR_BRIGHT] - fs = int(self.bounding_box.h * 0.07) - bb = self.menu_layout - t = self.factory.create_output_text(self.LOADING, bb, b, f, fs) - t.set_text(self.LOADING) - self.screen_title.set_text(name) - self.set_visible(True) - self.add_component(t) - self.clean_draw_update() - self.notify_loading_listeners() - - def reset_loading(self): - """ Remove Loading... sign """ - - del self.components[-1] - self.notify_loading_listeners() - - def add_loading_listener(self, listener): - """ Add loading listener - - :param listener: event listener - """ - if listener not in self.loading_listeners: - self.loading_listeners.append(listener) - - def notify_loading_listeners(self): - """ Notify all loading listeners """ - - for listener in self.loading_listeners: - listener(None) + Screen.set_loading(self, name, self.menu_layout) \ No newline at end of file diff --git a/ui/screen/podcastepisodes.py b/ui/screen/podcastepisodes.py new file mode 100644 index 00000000..4f3567cd --- /dev/null +++ b/ui/screen/podcastepisodes.py @@ -0,0 +1,206 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import math + +from ui.layout.borderlayout import BorderLayout +from ui.factory import Factory +from ui.page import Page +from ui.screen.menuscreen import MenuScreen +from ui.menu.menu import ALIGN_MIDDLE +from util.keys import SCREEN_RECT, KEY_PLAYER, KEY_BACK, FILE_BUTTON +from util.config import COLORS, COLOR_DARK_LIGHT +from ui.menu.multipagemenu import MultiPageMenu +from ui.menu.episodenavigator import EpisodeNavigator +from util.podcastsutil import STATUS_AVAILABLE, STATUS_LOADING, STATUS_LOADED, MENU_ROWS_EPISODES, \ + MENU_COLUMNS_EPISODES, PAGE_SIZE_EPISODES + +# 480x320 +PERCENT_TOP_HEIGHT = 14.0 +PERCENT_BOTTOM_HEIGHT = 14.0625 + +class PodcastEpisodesScreen(MenuScreen): + """ Podcast Episodes Screen """ + + def __init__(self, util, listeners, voice_assistant, state): + """ Initializer + + :param util: utility object + :param listeners: file browser listeners + :param voice_assistant: voice assistant + :param state: button state + """ + self.util = util + self.config = util.config + self.podcasts_util = util.get_podcasts_util() + self.listeners = listeners + self.factory = Factory(util) + self.bounding_box = self.config[SCREEN_RECT] + self.layout = BorderLayout(self.bounding_box) + self.layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_BOTTOM_HEIGHT, 0, 0) + + d = [MENU_ROWS_EPISODES, MENU_COLUMNS_EPISODES] + MenuScreen.__init__(self, util, listeners, MENU_ROWS_EPISODES, MENU_COLUMNS_EPISODES, voice_assistant, d, self.turn_page, page_in_title=False) + + if hasattr(state, "podcast_url"): + podcast_url = state.podcast_url + self.title = self.podcasts_util.summary_cache[podcast_url].name + else: + self.title = state.name + + m = self.factory.create_episode_menu_button + self.episodes_menu = MultiPageMenu(util, self.next_page, self.previous_page, self.set_title, self.reset_title, self.go_to_page, m, MENU_ROWS_EPISODES, MENU_COLUMNS_EPISODES, None, (0, 0, 0), self.menu_layout, align=ALIGN_MIDDLE) + self.set_menu(self.episodes_menu) + + self.total_pages = PAGE_SIZE_EPISODES * 2 + self.episodes = [] + self.navigator = EpisodeNavigator(self.util, self.layout.BOTTOM, listeners, self.config[COLORS][COLOR_DARK_LIGHT], self.total_pages) + self.components.append(self.navigator) + self.current_page = 1 + + self.save_episode_listeners = [] + + def set_current(self, state): + """ Set current state + + :param state: button state + """ + if not self.util.connected_to_internet and len(self.episodes) == 0 and not hasattr(state, "url"): + return + + if state.name == KEY_BACK or (getattr(state, "source", None) == FILE_BUTTON and len(self.episodes) != 0): + self.episodes_menu.clean_draw_update() + return + + if getattr(state, "podcast_url", None) != None: + self.episodes = self.podcasts_util.get_episodes(state.podcast_url) + if len(self.episodes) != 0: + self.total_pages = math.ceil(len(self.episodes) / PAGE_SIZE_EPISODES) + self.turn_page(state) + return + + self.set_loading(self.title) + if state.name != self.title or len(self.episodes) == 0: + self.title = state.name + if self.util.connected_to_internet: + self.episodes = self.podcasts_util.get_episodes(state.url) + else: + self.episodes = self.podcasts_util.get_episodes_from_disk(state.url) + + self.total_pages = math.ceil(len(self.episodes) / PAGE_SIZE_EPISODES) + + self.turn_page(state) + self.reset_loading() + + def turn_page(self, state=None): + """ Turn screen page + + :param state: button state + """ + filelist = Page(self.episodes, MENU_ROWS_EPISODES, MENU_COLUMNS_EPISODES) + + if state == None: + filelist.current_page_index = self.current_page - 1 + index = filelist.current_page_index * PAGE_SIZE_EPISODES + else: + if getattr(state, "status", None) == STATUS_LOADED: + if hasattr(state, "original_url") and len(state.original_url.strip()) > 0: + filelist.set_current_item_by_url(state.original_url) + else: + filelist.set_current_item_by_file_name(state.file_name) + else: + filelist.set_current_item_by_url(state.url) + index = filelist.current_item_index + + self.current_page = filelist.current_page_index + 1 + + page = filelist.get_current_page() + d = self.episodes_menu.make_dict(page) + self.episodes_menu.set_items(d, filelist.current_page_index, self.select_episode, False) + self.set_title(self.current_page) + self.episodes_menu.unselect() + self.episodes_menu.select_by_index(index) + + self.navigator.left_button.change_label(str(filelist.get_left_items_number())) + self.navigator.right_button.change_label(str(filelist.get_right_items_number())) + self.episodes_menu.clean_draw_update() + + if hasattr(self, "update_observer"): + self.episodes_menu.add_menu_observers(self.update_observer, self.redraw_observer) + + def select_episode(self, state): + """ Select podacst episode + + :param state: button state + """ + if state.long_press == True: + if state.status == STATUS_LOADED: + self.podcasts_util.delete_episode(state) + if not self.util.connected_to_internet: + for i, c in enumerate(self.episodes): + if c.name == state.name: + del self.episodes[i] + break + if len(self.episodes) == 0: + self.title = " " + self.set_title(0) + self.turn_page(state) + else: + state.icon_base = state.event_origin.components[1].content = self.podcasts_util.available_icon + state.status = STATUS_AVAILABLE + elif state.status == STATUS_AVAILABLE: + if self.podcasts_util.is_podcast_folder_available(): + state.icon_base = state.event_origin.components[1].content = self.podcasts_util.loading_icon + state.status = STATUS_LOADING + self.add_save_episode_listener(state) + self.podcasts_util.save_episode(state, self.notify_save_episode_listeners) + self.clean_draw_update() + else: + podcast_player = self.listeners[KEY_PLAYER] + podcast_player(state) + + def add_save_episode_listener(self, listener): + """ Add save episode listener + + :param listener: event listener + """ + if listener not in self.save_episode_listeners: + self.save_episode_listeners.append(listener) + + def notify_save_episode_listeners(self): + """ Notify all save episode listeners """ + + for index, listener in enumerate(self.save_episode_listeners): + listener.icon_base = listener.event_origin.components[1].content = self.podcasts_util.loaded_icon + listener.status = STATUS_LOADED + self.clean_draw_update() + if hasattr(self, "redraw_observer"): + self.redraw_observer() + del self.save_episode_listeners[index] + + def add_screen_observers(self, update_observer, redraw_observer): + """ Add screen observers + + :param update_observer: observer for updating the screen + :param redraw_observer: observer to redraw the whole screen + """ + MenuScreen.add_screen_observers(self, update_observer, redraw_observer) + self.update_observer = update_observer + self.redraw_observer = redraw_observer + self.add_loading_listener(redraw_observer) + self.navigator.add_observers(self.update_observer, self.redraw_observer) + \ No newline at end of file diff --git a/ui/screen/podcastplayer.py b/ui/screen/podcastplayer.py new file mode 100644 index 00000000..56218f67 --- /dev/null +++ b/ui/screen/podcastplayer.py @@ -0,0 +1,185 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import time + +from ui.state import State +from ui.screen.fileplayer import FilePlayerScreen +from util.config import PLAYER_SETTINGS, VOLUME, PODCASTS, PODCASTS_FOLDER, PODCAST_EPISODE_TIME, \ + MUTE, PAUSE, PODCAST_URL, PODCAST_EPISODE_NAME, PODCAST_EPISODE_URL +from util.fileutil import FILE_AUDIO +from util.keys import RESUME, ARROW_BUTTON, INIT +from util.podcastsutil import STATUS_LOADED + +class PodcastPlayerScreen(FilePlayerScreen): + """ Podcast Player Screen """ + + def __init__(self, listeners, util, get_current_playlist, voice_assistant, player_stop=None): + """ Initializer + + :param listeners: screen listeners + :param util: utility object + :param get_current_playlist: current playlist getter + :param voice_assistant: voice assistant + :param player_stop: stop player function + """ + self.podcasts_util = util.get_podcasts_util() + FilePlayerScreen.__init__(self, listeners, util, get_current_playlist, voice_assistant, player_stop) + self.file_button.state.name = "" + + def set_current(self, new_track=False, state=None): + """ Set current file or playlist + + :param new_track: True - new audio file + :param state: button state + """ + self.config[PODCASTS][PODCAST_EPISODE_NAME] = state.name + self.config[PODCASTS][PODCAST_URL] = state.podcast_url + if hasattr(state, "file_name"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.file_name + elif hasattr(state, "url"): + self.config[PODCASTS][PODCAST_EPISODE_URL] = state.url + + if getattr(state, "source", None) == INIT: + self.set_loading() + self.podcasts_util.init_playback(state) + self.reset_loading() + self.file_button.state.name = state.name + self.file_button.state.podcast_url = state.podcast_url + self.file_button.state.url = state.url + + self.screen_title.set_text(state.name) + + img = self.podcasts_util.get_podcast_image(state.podcast_image_url, 1.0, 1.0, self.bounding_box, state.online) + state.full_screen_image = img[1] + self.set_file_button(img) + + config_volume_level = int(self.config[PLAYER_SETTINGS][VOLUME]) + + if state: + state.volume = config_volume_level + + self.set_audio_file(new_track, state) + + if self.volume.get_position() != config_volume_level: + self.volume.set_position(config_volume_level) + self.volume.update_position() + + self.audio_files = None + + def get_audio_files(self): + """ Return the list of files + + :return: list of audio files + """ + current_podcast_url = self.config[PODCASTS][PODCAST_URL] + episodes = [] + try: + current_podcast = self.podcasts_util.summary_cache[current_podcast_url] + episodes = current_podcast.episodes + except: + pass + return episodes + + def is_valid_mode(self): + """ Check that current mode is valid mode + + :return: True - podcasts mode is valid + """ + return True + + def get_current_track_index(self, state=None): + """ Return current track index. + + :param state: button state + + :return: current track index + """ + for f in self.audio_files: + if f.file_name.startswith("http:") or f.file_name.startswith("https:"): + filename = f.file_name.split("/")[-1] + else: + filename = f.file_name.split("\\")[-1] + if filename == state["file_name"]: + return f.index + return 0 + + def set_audio_file(self, new_track, s=None): + """ Set new audio file + + :param new_track: True - new file + "param s" button state + """ + state = State() + state.folder = PODCASTS_FOLDER + + if s.status == STATUS_LOADED: + state.url = s.file_name + state.original_url = s.url + else: + state.url = s.url + + state.mute = self.config[PLAYER_SETTINGS][MUTE] + state.pause = self.config[PLAYER_SETTINGS][PAUSE] + state.file_type = FILE_AUDIO + state.dont_notify = True + state.source = FILE_AUDIO + state.playback_mode = FILE_AUDIO + state.status = s.status + if hasattr(s, "file_name"): + state.file_name = s.file_name + source = None + if s: + source = getattr(s, "source", None) + + if new_track: + tt = 0.0 + else: + tt = self.config[PODCASTS][PODCAST_EPISODE_TIME] + + if (isinstance(tt, str) and len(tt) != 0) or (isinstance(tt, float) or (source and source == RESUME)) or isinstance(tt, int): + state.track_time = tt + + self.time_control.slider.set_position(0) + + if self.file_button and self.file_button.components[1] and self.file_button.components[1].content: + state.icon_base = self.file_button.components[1].content + + if s and s.volume: + state.volume = s.volume + + if getattr(s, "full_screen_image", None) != None: + state.full_screen_image = s.full_screen_image + + self.notify_play_listeners(state) + + def change_track(self, track_index): + """ Change track + + :param track_index: track index + """ + s = self.audio_files[track_index] + self.stop_timer() + time.sleep(0.3) + s.source = ARROW_BUTTON + self.set_current(True, s) + + def start_timer(self): + """ Start time control timer """ + + self.time_control.start_timer() + \ No newline at end of file diff --git a/ui/screen/podcasts.py b/ui/screen/podcasts.py new file mode 100644 index 00000000..f22abbdc --- /dev/null +++ b/ui/screen/podcasts.py @@ -0,0 +1,134 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import os +import math + +from ui.layout.borderlayout import BorderLayout +from ui.factory import Factory +from ui.screen.menuscreen import MenuScreen +from ui.menu.menu import ALIGN_MIDDLE +from util.keys import SCREEN_RECT, KEY_PODCAST_EPISODES +from util.config import COLORS, COLOR_DARK_LIGHT, LABELS, PODCASTS, PODCAST_URL +from ui.menu.multipagemenu import MultiPageMenu +from util.podcastsutil import PodcastsUtil, MENU_ROWS_PODCASTS, MENU_COLUMNS_PODCASTS, PAGE_SIZE_PODCASTS +from ui.menu.podcastnavigator import PodcastNavigator + +# 480x320 +PERCENT_TOP_HEIGHT = 14.0625 +PERCENT_BOTTOM_HEIGHT = 14.0625 + +class PodcastsScreen(MenuScreen): + """ Podcasts Screen """ + + def __init__(self, util, listeners, voice_assistant): + """ Initializer + + :param util: utility object + :param listeners: listeners + :param voice_assistant: voice assistant + """ + self.util = util + self.config = util.config + self.listeners = listeners + self.factory = Factory(util) + + self.podcasts_util = util.get_podcasts_util() + self.bounding_box = self.config[SCREEN_RECT] + layout = BorderLayout(self.bounding_box) + layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_BOTTOM_HEIGHT, 0, 0) + + d = [MENU_ROWS_PODCASTS, MENU_COLUMNS_PODCASTS] + MenuScreen.__init__(self, util, listeners, MENU_ROWS_PODCASTS, MENU_COLUMNS_PODCASTS, voice_assistant, d, self.turn_page, page_in_title=False, show_loading=True) + self.title = self.config[LABELS][PODCASTS] + + m = self.factory.create_podcast_menu_button + self.podcasts_menu = MultiPageMenu(util, self.next_page, self.previous_page, self.set_title, self.reset_title, self.go_to_page, m, MENU_ROWS_PODCASTS, MENU_COLUMNS_PODCASTS, None, (0, 0, 0), self.menu_layout, align=ALIGN_MIDDLE) + self.set_menu(self.podcasts_menu) + + self.navigator = PodcastNavigator(self.util, self.layout.BOTTOM, listeners, self.config[COLORS][COLOR_DARK_LIGHT], PAGE_SIZE_PODCASTS + 1) + self.components.append(self.navigator) + + url = self.config[PODCASTS][PODCAST_URL] + if url and len(url) > 0: + self.current_page = self.podcasts_util.get_podcast_page(url, PAGE_SIZE_PODCASTS) + else: + self.current_page = 1 + + def set_current(self, state): + """ Set current state + + :param state: button state + """ + if self.util.connected_to_internet: + podcast_links_num = len(self.podcasts_util.get_podcasts_links()) + else: + podcast_links_num = len(self.podcasts_util.load_podcasts()) + + self.total_pages = math.ceil(podcast_links_num / PAGE_SIZE_PODCASTS) + + self.set_loading(self.title) + self.turn_page() + self.reset_loading() + + def turn_page(self): + """ Turn podcasts page """ + + page = {} + if self.util.connected_to_internet: + page = self.podcasts_util.get_podcasts(self.current_page, PAGE_SIZE_PODCASTS) + + if len(list(page.keys())) == 0 or not self.util.connected_to_internet: + page = self.podcasts_util.get_podcasts_from_disk(self.current_page, PAGE_SIZE_PODCASTS) + + self.podcasts_menu.set_items(page, 0, self.listeners[KEY_PODCAST_EPISODES], False) + + keys = list(page.keys()) + + if len(keys) != 0: + self.podcasts_menu.item_selected(page[keys[0]]) + if self.navigator and self.total_pages > 1: + self.navigator.left_button.change_label(str(self.current_page - 1)) + self.navigator.right_button.change_label(str(self.total_pages - self.current_page)) + + self.set_title(self.current_page) + self.podcasts_menu.clean_draw_update() + + if hasattr(self, "update_observer"): + self.podcasts_menu.add_menu_observers(self.update_observer, self.redraw_observer) + + self.podcasts_menu.unselect() + for i, b in enumerate(self.podcasts_menu.buttons.values()): + url = self.config[PODCASTS][PODCAST_URL] + if url == b.state.url: + self.podcasts_menu.select_by_index(i) + return + self.podcasts_menu.select_by_index(0) + + def add_screen_observers(self, update_observer, redraw_observer): + """ Add screen observers + + :param update_observer: observer for updating the screen + :param redraw_observer: observer to redraw the whole screen + """ + MenuScreen.add_screen_observers(self, update_observer, redraw_observer) + self.update_observer = update_observer + self.redraw_observer = redraw_observer + self.add_loading_listener(redraw_observer) + for b in self.navigator.menu_buttons: + self.add_button_observers(b, update_observer, redraw_observer) + \ No newline at end of file diff --git a/ui/screen/screen.py b/ui/screen/screen.py index 2b2f2c52..a2a3f208 100644 --- a/ui/screen/screen.py +++ b/ui/screen/screen.py @@ -18,8 +18,8 @@ from ui.container import Container from ui.factory import Factory from ui.layout.borderlayout import BorderLayout -from util.util import LABELS -from util.config import SCREEN_RECT, COLORS, COLOR_CONTRAST, USAGE, USE_VOICE_ASSISTANT, \ +from util.keys import KEY_LOADING, LABELS +from util.config import SCREEN_RECT, COLORS, COLOR_CONTRAST, USAGE, USE_VOICE_ASSISTANT, COLOR_DARK, COLOR_BRIGHT, \ KEY_VA_START, KEY_VA_STOP, KEY_WAITING_FOR_COMMAND, KEY_VA_COMMAND, COLOR_DARK_LIGHT PERCENT_TOP_HEIGHT = 14.00 @@ -85,6 +85,9 @@ def __init__(self, util, title_key, percent_bottom_height=0, voice_assistant=Non self.player_screen = False self.update_web_observer = None self.update_web_title = None + + self.loading_listeners = [] + self.LOADING = util.config[LABELS][KEY_LOADING] def draw_title_bar(self): """ Draw title bar on top of the screen """ @@ -177,3 +180,48 @@ def add_screen_observers(self, update_observer, redraw_observer, title_to_json=N """ self.update_web_observer = redraw_observer self.update_web_title = title_to_json + + def set_loading(self, name=None, menu_bb=None): + """ Show Loading... sign + + :name: screen title + """ + b = self.config[COLORS][COLOR_DARK] + f = self.config[COLORS][COLOR_BRIGHT] + fs = int(self.bounding_box.h * 0.07) + + if menu_bb != None: + bb = menu_bb + else: + bb = self.bounding_box + + t = self.factory.create_output_text(self.LOADING, bb, b, f, fs) + t.set_text(self.LOADING) + + if name != None: + self.screen_title.set_text(name) + + self.set_visible(True) + self.add_component(t) + self.clean_draw_update() + self.notify_loading_listeners() + + def reset_loading(self): + """ Remove Loading label """ + + del self.components[-1] + self.notify_loading_listeners() + + def add_loading_listener(self, listener): + """ Add loading listener + + :param listener: event listener + """ + if listener not in self.loading_listeners: + self.loading_listeners.append(listener) + + def notify_loading_listeners(self): + """ Notify all loading listeners """ + + for listener in self.loading_listeners: + listener(None) diff --git a/ui/slider/slider.py b/ui/slider/slider.py index d3e4bed6..fe5753a2 100644 --- a/ui/slider/slider.py +++ b/ui/slider/slider.py @@ -22,6 +22,7 @@ from ui.component import Component from ui.container import Container from ui.state import State +from util.config import PLAYER_SETTINGS, PAUSE HORIZONTAL = "1" VERTICAL = "2" @@ -299,6 +300,9 @@ def handle_event(self, event): :param event: event to handle """ + if self.util.config[PLAYER_SETTINGS][PAUSE]: + return + if not self.visible: return mouse_events = [pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN, pygame.MOUSEMOTION] diff --git a/ui/slider/timeslider.py b/ui/slider/timeslider.py index 62c3d9f1..9676292f 100644 --- a/ui/slider/timeslider.py +++ b/ui/slider/timeslider.py @@ -24,7 +24,7 @@ from ui.slider.slider import Slider from ui.layout.borderlayout import BorderLayout from util.config import CURRENT_FILE, USAGE, USE_WEB, BROWSER_TRACK_FILENAME, AUDIOBOOKS, COLORS, \ - COLOR_BRIGHT, FILE_PLAYBACK, CD_PLAYBACK, CD_TRACK + COLOR_BRIGHT, FILE_PLAYBACK, CD_PLAYBACK, CD_TRACK, PODCASTS, PODCAST_EPISODE_URL from ui.state import State class TimeSlider(Container): @@ -237,11 +237,11 @@ def slider_action_handler(self, evt): :param evt: event """ - a = self.config[FILE_PLAYBACK][CURRENT_FILE] b = self.config[AUDIOBOOKS][BROWSER_TRACK_FILENAME] c = self.config[CD_PLAYBACK][CD_TRACK] - if not (a or b or c): + d = self.config[PODCASTS][PODCAST_EPISODE_URL] + if not (a or b or c or d): return if not self.timer_started: diff --git a/util/config.py b/util/config.py index 7acb4d01..fb03c567 100644 --- a/util/config.py +++ b/util/config.py @@ -73,6 +73,8 @@ COVER_ART_FOLDERS = "cover.art.folders" AUTO_PLAY_NEXT_TRACK = "auto.play.next.track" CYCLIC_PLAYBACK = "cyclic.playback" +HIDE_FOLDER_NAME = "hide.folder.name" +FOLDER_IMAGE_SCALE_RATIO = "folder.image.scale.ratio" WEB_SERVER = "web.server" HTTP_PORT = "http.port" @@ -80,6 +82,13 @@ STREAM_SERVER = "stream.server" STREAM_SERVER_PORT = "stream.server.port" +PODCASTS_FOLDER = "podcasts.folder" +PODCASTS = "podcasts" +PODCAST_URL = "podcast.url" +PODCAST_EPISODE_NAME = "podcast.episode.name" +PODCAST_EPISODE_URL = "podcast.episode.url" +PODCAST_EPISODE_TIME = "podcast.episode.time" + HOME_MENU = "home.menu" RADIO = "radio" AUDIO_FILES = "audio-files" @@ -94,6 +103,7 @@ WAKE_UP_TIME = "wake.up.time" POWEROFF = "poweroff" SLEEP_NOW = "sleep-now" +LOADING = "loading" VOICE_ASSISTANT = "voice.assistant" VOICE_ASSISTANT_TYPE = "type" @@ -114,6 +124,10 @@ FONT_SECTION = "font" FONT_KEY = "font.name" +SCRIPTS = "scripts" +STARTUP = "startup.script.name" +SHUTDOWN = "shutdown.script.name" + SCREENSAVER_MENU = "screensaver.menu" CLOCK = "clock" LOGO = "logo" @@ -307,6 +321,8 @@ def load_config(self, config): config[COVER_ART_FOLDERS] = self.get_list(config_file, FILE_BROWSER, COVER_ART_FOLDERS) config[AUTO_PLAY_NEXT_TRACK] = config_file.getboolean(FILE_BROWSER, AUTO_PLAY_NEXT_TRACK) config[CYCLIC_PLAYBACK] = config_file.getboolean(FILE_BROWSER, CYCLIC_PLAYBACK) + config[HIDE_FOLDER_NAME] = config_file.getboolean(FILE_BROWSER, HIDE_FOLDER_NAME) + config[FOLDER_IMAGE_SCALE_RATIO] = float(config_file.get(FILE_BROWSER, FOLDER_IMAGE_SCALE_RATIO)) c = {USE_LIRC : config_file.getboolean(USAGE, USE_LIRC)} c[USE_TOUCHSCREEN] = config_file.getboolean(USAGE, USE_TOUCHSCREEN) @@ -354,11 +370,14 @@ def load_config(self, config): c = {STREAM_SERVER_PORT : config_file.get(STREAM_SERVER, STREAM_SERVER_PORT)} config[STREAM_SERVER] = c + config[PODCASTS_FOLDER] = config_file.get(PODCASTS, PODCASTS_FOLDER) + c = {RADIO: config_file.getboolean(HOME_MENU, RADIO)} c[AUDIO_FILES] = config_file.getboolean(HOME_MENU, AUDIO_FILES) c[AUDIOBOOKS] = config_file.getboolean(HOME_MENU, AUDIOBOOKS) c[STREAM] = config_file.getboolean(HOME_MENU, STREAM) c[CD_PLAYER] = config_file.getboolean(HOME_MENU, CD_PLAYER) + c[PODCASTS] = config_file.getboolean(HOME_MENU, PODCASTS) c[EQUALIZER] = config_file.getboolean(HOME_MENU, EQUALIZER) c[TIMER] = config_file.getboolean(HOME_MENU, TIMER) config[HOME_MENU] = c @@ -380,6 +399,11 @@ def load_config(self, config): config[COLORS] = c config[FONT_KEY] = config_file.get(FONT_SECTION, FONT_KEY) + + c = {} + c[STARTUP] = config_file.get(SCRIPTS, STARTUP) + c[SHUTDOWN] = config_file.get(SCRIPTS, SHUTDOWN) + config[SCRIPTS] = c c = {CLOCK: config_file.getboolean(SCREENSAVER_MENU, CLOCK)} c[LOGO] = config_file.getboolean(SCREENSAVER_MENU, LOGO) @@ -533,6 +557,12 @@ def load_current(self, config): c[CD_TRACK_TIME] = config_file.get(CD_PLAYBACK, CD_TRACK_TIME) config[CD_PLAYBACK] = c + c = {PODCAST_URL: config_file.get(PODCASTS, PODCAST_URL)} + c[PODCAST_EPISODE_NAME] = config_file.get(PODCASTS, PODCAST_EPISODE_NAME) + c[PODCAST_EPISODE_URL] = config_file.get(PODCASTS, PODCAST_EPISODE_URL) + c[PODCAST_EPISODE_TIME] = config_file.get(PODCASTS, PODCAST_EPISODE_TIME) + config[PODCASTS] = c + for language in config[KEY_LANGUAGES]: n = language[NAME] k = STATIONS + "." + n @@ -622,7 +652,7 @@ def save_current_settings(self): config_parser.optionxform = str config_parser.read_file(codecs.open(FILE_CURRENT, "r", UTF8)) - a = b = c = d = e = f = g = stations_changed = None + a = b = c = d = e = f = g = h = stations_changed = None if self.config[USAGE][USE_AUTO_PLAY]: a = self.save_section(CURRENT, config_parser) @@ -637,12 +667,13 @@ def save_current_settings(self): if z: stations_changed = True f = self.save_section(AUDIOBOOKS, config_parser) + h = self.save_section(PODCASTS, config_parser) b = self.save_section(PLAYER_SETTINGS, config_parser) e = self.save_section(SCREENSAVER, config_parser) g = self.save_section(TIMER, config_parser) - if a or b or c or d or e or f or g or stations_changed: + if a or b or c or d or e or f or g or h or stations_changed: with codecs.open(FILE_CURRENT, 'w', UTF8) as file: config_parser.write(file) diff --git a/util/favoritesutil.py b/util/favoritesutil.py index d487f0b7..5ca920de 100644 --- a/util/favoritesutil.py +++ b/util/favoritesutil.py @@ -47,6 +47,10 @@ def is_favorite_mode(self): """ lang = self.config[CURRENT][LANGUAGE] k = STATIONS + "." + lang + + if len(self.config[k]) == 0: + return False + group = self.config[k][CURRENT_STATIONS] if group == KEY_FAVORITES: return True @@ -121,6 +125,9 @@ def mark_favorites(self, buttons): :param buttons: buttons to mark """ + if len(self.config[STATIONS + "." + self.config[CURRENT][LANGUAGE]]) == 0: + return + group = self.config[STATIONS + "." + self.config[CURRENT][LANGUAGE]][CURRENT_STATIONS] if group == KEY_FAVORITES: return diff --git a/util/keys.py b/util/keys.py index 6b827688..a0223df4 100644 --- a/util/keys.py +++ b/util/keys.py @@ -53,6 +53,7 @@ SCREEN_RECT = "screen.rect" LINUX_PLATFORM = "linux" WINDOWS_PLATFORM = "windows" +H_ALIGN = "h_align" H_ALIGN_LEFT = 0 H_ALIGN_CENTER = 1 H_ALIGN_RIGHT = 2 @@ -91,11 +92,14 @@ KEY_PARENT = "parent" KEY_USER_HOME = "user-home" KEY_PLAYER = "player" +KEY_PODCAST_PLAYER = "podcast.player" +KEY_PODCASTS_MENU = "podcasts-menu" KEY_EJECT = "eject" KEY_REFRESH = "refresh" KEY_CD_DRIVES = "cd.drives" KEY_CD_PLAYERS = "cd-players" KEY_CD_TRACKS = "cd-tracks" +KEY_PODCAST_EPISODES = "podcast.episodes" KEY_0 = "0" KEY_1 = "1" KEY_2 = "2" @@ -137,6 +141,7 @@ ARROW_BUTTON = "arror.button" INIT = "init" RESUME = "resume" +FILE_BUTTON = "file.button" CLASSICAL = "classical" JAZZ = "jazz" diff --git a/util/podcastsutil.py b/util/podcastsutil.py new file mode 100644 index 00000000..4fd9100e --- /dev/null +++ b/util/podcastsutil.py @@ -0,0 +1,712 @@ +# Copyright 2019 Peppy Player peppy.player@gmail.com +# +# This file is part of Peppy Player. +# +# Peppy Player is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Peppy Player is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Peppy Player. If not, see . + +import os +import feedparser +import requests +import codecs +import json + +from threading import Thread +from util.keys import * +from ui.state import State +from ui.layout.borderlayout import BorderLayout +from ui.screen.screen import PERCENT_TOP_HEIGHT, PERCENT_TITLE_FONT +from ui.screen.menuscreen import PERCENT_TOP_HEIGHT as PERCENT_TOP_HEIGHT_MENU_SCREEN +from ui.menu.menu import Menu +from util.config import PODCASTS, AUDIO_FILES, LOADING, PODCASTS_FOLDER, COLORS, COLOR_DARK, UTF8, SCREEN_RECT + +FOLDER_PODCASTS = "podcasts" +FILE_PODCASTS = "podcasts.m3u" +FILE_DEFAULT_PODCAST = "podcasts.svg" +FILE_PODCASTS_JSON = "podcasts.json" + +STATUS_AVAILABLE = "available" +STATUS_LOADING = "loading" +STATUS_LOADED = "loaded" + +MENU_ROWS_EPISODES = 3 +MENU_COLUMNS_EPISODES = 1 +PAGE_SIZE_EPISODES = MENU_ROWS_EPISODES * MENU_COLUMNS_EPISODES + +MENU_ROWS_PODCASTS = 2 +MENU_COLUMNS_PODCASTS = 1 +PAGE_SIZE_PODCASTS = MENU_ROWS_PODCASTS * MENU_COLUMNS_PODCASTS + +class PodcastsUtil(object): + """ Podcasts Utility class """ + + def __init__(self, util): + """ Initializer + + :param util: utility object + """ + self.util = util + self.config = util.config + self.podcasts_links = None + self.summary_cache = {} + self.loading = [] + self.available_icon = None + self.loading_icon = None + self.loaded_icon = None + self.podcast_image_cache = {} + self.podcasts_json = [] + + layout = BorderLayout(self.config[SCREEN_RECT]) + layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_TOP_HEIGHT_MENU_SCREEN, 0, 0) + self.episode_button_font_size = int((layout.TOP.h * PERCENT_TITLE_FONT)/100.0) + tmp = Menu(util, (0, 0, 0), layout.CENTER, MENU_ROWS_EPISODES, MENU_COLUMNS_EPISODES) + layout = tmp.get_layout([1]*PAGE_SIZE_EPISODES) + self.episode_button_bb = layout.get_next_constraints() + + layout = BorderLayout(self.config[SCREEN_RECT]) + layout.set_percent_constraints(PERCENT_TOP_HEIGHT, PERCENT_TOP_HEIGHT_MENU_SCREEN, 0, 0) + self.podcast_button_font_size = int((layout.TOP.h * PERCENT_TITLE_FONT)/100.0) + tmp = Menu(util, (0, 0, 0), layout.CENTER, MENU_ROWS_PODCASTS, MENU_COLUMNS_PODCASTS) + layout = tmp.get_layout([1]*PAGE_SIZE_PODCASTS) + self.podcast_button_bb = layout.get_next_constraints() + layout = tmp = None + + def init_playback(self, state): + """ Initialize podcasts playback + + :param state: button state + """ + url = state.podcast_url + if self.util.connected_to_internet: + s = self.get_podcast_info(0, url) + self.get_episodes(url) + state.online = True + else: + s = self.get_podcasts_from_disk(0, 2) + self.get_episodes_from_disk(url) + state.online = False + + state.status = state.file_name = self.get_episode_status(url, state.url) + state.podcast_image_url = self.summary_cache[url].episodes[0].podcast_image_url + + def get_episode_status(self, podcast_url, episode_url): + """ Episode status getter + + :param podcast_url: podcast url + :param episode_url: episode url + + :return: episode status + """ + episodes = self.summary_cache[podcast_url].episodes + for episode in episodes: + if episode.url == episode_url: + return episode.status + return None + + def get_podcasts_links(self): + """ Get podcasts links from file + + :return: list of podcasts URLs + """ + if self.podcasts_links != None: + return self.podcasts_links + + path = os.path.join(os.getcwd(), FOLDER_PODCASTS, FILE_PODCASTS) + self.podcasts_links = [] + + try: + lines = codecs.open(path, "r", UTF8).read().split("\n") + except Exception as e: + pass + + for line in lines: + if len(line.strip()) == 0 or line.strip().startswith("#"): + continue + self.podcasts_links.append(line.strip()) + + return self.podcasts_links + + def load_podcasts(self): + """ Load list of all loaded podcasts/episodes from file + + :return: list of downloaded + """ + podcast_folder = self.config[PODCASTS_FOLDER] + if len(podcast_folder.strip()) == 0: + return [] + + podacsts_file = os.path.join(podcast_folder, FILE_PODCASTS_JSON) + + d = [] + try: + with open(podacsts_file) as f: + try: + d = json.load(f) + except: + pass + except: + pass + + return d + + def get_podcasts(self, page, page_size): + """ Get dictionary with podcasts state objects + + :param page: page number + :param page_size: number of buttons per page + + :return: dictionary with state objects + """ + result = {} + links = self.get_podcasts_links() + if len(links) == 0: + return result + + start_index = (page - 1) * page_size + end_index = start_index + page_size + + try: + self.podcasts_json = self.load_podcasts() + except: + pass + + for i, link in enumerate(links[start_index : end_index]): + try: + p = self.summary_cache[link] + result[link] = p + continue + except: + pass + + result[link] = self.get_podcast_info(i, link) + + return result + + def get_podcast_info(self, index, podcast_url): + """ Get podcast info as state object + + :param index: podcast index + :param podcast_url: podcast url + + :return: podcast info as State object + """ + try: + response = requests.get(podcast_url) + rss = feedparser.parse(response.content) + except: + return + + s = State() + s.index = index + s.name = rss.feed.title + s.l_name = s.name + s.description = rss.feed.subtitle + s.url = podcast_url + s.online = True + s.fixed_height = int(self.podcast_button_font_size * 0.8) + s.file_type = PODCASTS + s.comparator_item = s.index + s.bgr = self.config[COLORS][COLOR_DARK] + s.show_bgr = True + + if 'image' in rss.feed and 'href' in rss.feed.image: + img = rss.feed.image.href.strip() + else: + img = '' + + s.image_name = img + s.icon_base = self.get_podcast_image(img, 0.5, 0.8, self.podcast_button_bb) + self.summary_cache[s.url] = s + + return s + + def get_podcasts_from_disk(self, page, page_size): + """ Get one page of loaded podcasts + + :param page: page number + :page_size: page size + + :return: dictionary of podcasts where the key is the podcast name + """ + result = {} + self.podcasts_json = self.load_podcasts() + if len(self.podcasts_json) == 0: + return result + + start_index = (page - 1) * page_size + end_index = start_index + page_size + + for i, podcast in enumerate(self.podcasts_json[start_index : end_index]): + try: + p = self.summary_cache[podcast] + result[podcast] = p + continue + except: + pass + + result[podcast["name"]] = self.get_podcast_info_from_disk(i, podcast) + + return result + + def get_podcast_info_from_disk(self, index, podcast): + """ Get the info of loaded podcast as State object + + :param index: podcast index + :param podcast: podcast dictionary + + :return: podcast info as State object + """ + s = State() + s.index = index + s.name = podcast["name"] + s.l_name = s.name + s.url = podcast["url"] + s.online = False + s.description = podcast["summary"] + s.fixed_height = int(self.podcast_button_font_size * 0.8) + s.file_type = PODCASTS + s.comparator_item = s.index + s.bgr = self.config[COLORS][COLOR_DARK] + s.show_bgr = True + + try: + img = os.path.join(self.config[PODCASTS_FOLDER], podcast["image"]) + except: + img = '' + + s.image_name = img + s.icon_base = self.get_podcast_image(img, 0.5, 0.8, self.podcast_button_bb, False) + self.summary_cache[s.url] = s + + return s + + def get_podcast_image(self, img_name, k, f, bb, online=True): + """ Get podcast image + + :param img_name: image filename + :param k: scale factor + :param f: scale ratio + :param b: bounding box + :param online: online/offline flag + + :return: podcast image + """ + podcast_image = None + + if len(img_name) != 0: + cache_key = img_name + str(k) + str(f) + try: + podcast_image = self.podcast_image_cache[cache_key] + except: + pass + + if podcast_image != None: + return podcast_image + + podcast_image = self.util.load_mono_svg_icon(PODCASTS, self.util.COLOR_MAIN, bb, k) + cache_key = PODCASTS + str(k) + str(f) + if len(img_name) != 0: + if online: + image = self.util.load_image_from_url(img_name, True) + else: + image = self.util.load_image(img_name) + + if image != None: + factor = 0.8 + scale_ratio = self.util.get_scale_ratio((bb.w * f, bb.h * f), image[1], fit_height=True) + podcast_image = (img_name, self.util.scale_image(image, scale_ratio)) + cache_key = img_name + str(k) + str(f) + + self.podcast_image_cache[cache_key] = podcast_image + return podcast_image + + def get_podcast_page(self, url, page_size): + """ Define podcast page number by its URL + + :param url: podcast url + :param page_size: page size + + :return: page number for specified podcast + """ + index = 0 + links = self.get_podcasts_links() + for i, link in enumerate(links): + if url == link: + index = i + break + n = int(i/page_size) + r = int(i%page_size) + if r > 0: + n += 1 + return n + 1 + + def set_episode_icon(self, episode_name, bb, s, online=True): + """ Set icon, status and file name on provided episode state object + + :param episode_name: episode name + :param bb: bounding box + :param s: state object + :param online: episode status + """ + podcast_folder = self.config[PODCASTS_FOLDER] + + if not online: + filename = s.file_name + episode_file = os.path.join(podcast_folder, s.podcast_name, filename) + self.loaded_icon = self.util.load_mono_svg_icon(AUDIO_FILES, self.util.COLOR_MAIN, bb, 0.4) + s.icon_base = self.loaded_icon + s.status = STATUS_LOADED + s.file_name = episode_file + return + + filename = s.url.split('/')[-1] + episode_file = os.path.join(podcast_folder, filename) + + if self.available_icon == None: + self.available_icon = self.util.load_mono_svg_icon(PODCASTS, self.util.COLOR_MAIN, bb, 0.4) + self.loading_icon = self.util.load_mono_svg_icon(LOADING, self.util.COLOR_MAIN, bb, 0.4) + self.loaded_icon = self.util.load_mono_svg_icon(AUDIO_FILES, self.util.COLOR_MAIN, bb, 0.4) + + if episode_name in self.loading: + s.icon_base = self.loading_icon + s.status = STATUS_LOADING + s.file_name = "" + elif (self.is_episode_saved(s)) or not online: + s.icon_base = self.loaded_icon + s.status = STATUS_LOADED + s.file_name = episode_file + else: + s.icon_base = self.available_icon + s.status = STATUS_AVAILABLE + s.file_name = s.url + + def clean_summary(self, summary): + """ Clean provide summary from special characters + + "param summary": summary string to clean + + :return: clean summary string + """ + s = summary.replace("

", "").replace("

", "") + s = s.replace("", "").replace("", "") + s = s.replace("", "").replace("", "") + s = s.replace("", "") + s = s.replace("\">", " ").replace("\\n", " ") + s = s.replace("'", "'").replace("
", "") + return s.replace("", "").replace("", "") + + def get_episodes(self, podcast_url): + """ Get podcast episodes + + :param podcast_url: podcast URL + + :return: dictionary with episodes + """ + try: + podcast = self.summary_cache[podcast_url] + podcast_image_url = podcast.image_name + episodes = podcast.episodes + return episodes + except: + pass + + episodes = [] + rss = feedparser.parse(podcast_url) + if rss == None: + return episodes + + entries = rss.entries + + for i, entry in enumerate(entries): + try: + enclosure = entry.enclosures[0] + except: + continue + s = State() + s.index = i + s.name = entry.title + s.l_name = s.name + s.url = enclosure.href + s.length = enclosure.length + s.type = enclosure.type + s.description = self.clean_summary(entry.summary) + s.fixed_height = int(self.episode_button_font_size * 0.8) + s.file_type = PODCASTS + s.online = podcast.online + s.comparator_item = s.index + s.bgr = self.config[COLORS][COLOR_DARK] + s.show_bgr = True + s.podcast_name = podcast.name + s.podcast_url = podcast_url + s.podcast_image_url = podcast_image_url + episode_name = s.url.split("/")[-1] + self.set_episode_icon(episode_name, self.episode_button_bb, s) + episodes.append(s) + + self.summary_cache[podcast_url].episodes = episodes + return episodes + + def get_episodes_from_disk(self, podcast_url): + """ Get podcast episodes from disk + + :param podcast_url: podcast URL + + :return: dictionary with episodes + """ + try: + podcast = self.summary_cache[podcast_url] + podcast_image_url = podcast.image_name + episodes = podcast.episodes + return episodes + except: + pass + + episodes = [] + podcast = self.summary_cache[podcast_url] + + entries = [] + for p in self.podcasts_json: + if p["url"] == podcast_url: + try: + entries = p["episodes"] + except: + pass + if len(entries) == 0: + return [] + + for i, entry in enumerate(entries): + s = State() + s.index = i + s.name = entry["name"] + s.l_name = s.name + s.file_name = entry["filename"] + s.description = entry["summary"] + s.fixed_height = int(self.episode_button_font_size * 0.8) + s.file_type = PODCASTS + s.online = podcast.online + s.comparator_item = s.index + s.bgr = self.config[COLORS][COLOR_DARK] + s.show_bgr = True + s.podcast_url = podcast_url + s.podcast_name = podcast.name + s.url = "" + s.podcast_image_url = podcast_image_url + self.set_episode_icon(s.name, self.episode_button_bb, s, False) + episodes.append(s) + + self.summary_cache[podcast_url].episodes = episodes + return episodes + + def is_episode_saved(self, s): + """ Check if episode was saved or not + + :param s: episode state object + + :return: True - episode was saved, False - not saved + """ + if len(self.podcasts_json) == 0: + try: + self.podcasts_json = self.load_podcasts() + except: + pass + if len(self.podcasts_json) == 0: + return False + + for p in self.podcasts_json: + if p["url"] == s.podcast_url: + try: + episodes = p["episodes"] + for e in episodes: + if e["name"] == s.name: + return True + except: + pass + return False + + def save_episode(self, state, callback): + """ Start saving thread """ + + thread = Thread(target=self.save, args=[state, callback]) + thread.start() + + def save(self, button_state, callback): + """ Episode saving thread + + :param button_state: state object defining episode details + :param callback: callback function to call when saving finished + """ + podcast_folder = self.config[PODCASTS_FOLDER] + url = button_state.url + filename = url.split('/')[-1] + self.loading.append(filename) + + episode_file = os.path.join(podcast_folder, filename) + self.save_file_from_web(episode_file, url) + + url = button_state.podcast_image_url + if url.startswith("http"): + f = url.split("/")[-1] + image_file = os.path.join(podcast_folder, f) + self.save_file_from_web(image_file, url) + + self.loading.remove(filename) + button_state.file_name = episode_file + self.cache_episode(button_state) + callback() + + def save_file_from_web(self, filename, url): + """ Save file from web + + :param filename: file name + :param url: URL of the file to save + """ + r = requests.get(url, stream=True) + size = 4096 + with open(filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=size): + if chunk: + f.write(chunk) + + def cache_episode(self, state): + """ Cache episode + + :param state: state object defining episode + """ + summary = self.clean_summary(state.description) + episode = { + "name": state.name, + "filename": state.file_name, + "summary": summary + } + + podcast = None + for p in self.podcasts_json: + if p["name"] == state.podcast_name: + podcast = p + break + + if podcast == None: + p = self.summary_cache[state.podcast_url] + podcast_json = { + "name": p.name, + "url": p.url, + "summary": self.clean_summary(p.description), + "image": p.image_name.split("/")[-1], + "episodes": [episode] + } + self.podcasts_json.append(podcast_json) + else: + try: + episodes = podcast["episodes"] + episodes.append(episode) + except: + podcast["episodes"] = episode + + self.save_podcasts_json() + + def delete_episode(self, button_state): + """ Delete episode from disk and cache + + :param button_state: state object defining episode to delete + """ + podcast_folder = self.config[PODCASTS_FOLDER] + + if button_state.online: + url = button_state.url + filename = url.split('/')[-1] + episode_file = os.path.join(podcast_folder, filename) + else: + episode_file = button_state.file_name + + try: + os.remove(episode_file) + except: + pass + + podcast = None + for p in self.podcasts_json: + if p["name"] == button_state.podcast_name: + podcast = p + break + + if podcast == None: + return + + try: + episodes = podcast["episodes"] + for i, e in enumerate(episodes): + if e["name"] == button_state.name: + del episodes[i] + if len(episodes) == 0: + for i, p in enumerate(self.podcasts_json): + if p["name"] == button_state.podcast_name: + del self.podcasts_json[i] + self.save_podcasts_json() + self.delete_podcast_icon(p) + else: + self.save_podcasts_json() + except: + pass + + def delete_podcast_icon(self, p): + """ Delete podcast icon from disk + + :param p: dictionary defining podcast + """ + podcast_folder = self.config[PODCASTS_FOLDER] + filename = p["image"] + path = os.path.join(podcast_folder, filename) + os.remove(path) + + def save_podcasts_json(self): + """ Save podcasts Json file """ + + podcast_folder = self.config[PODCASTS_FOLDER] + filename = os.path.join(podcast_folder, FILE_PODCASTS_JSON) + + if len(self.podcasts_json) == 0: + os.remove(filename) + else: + with open(filename, 'w') as f: + json.dump(self.podcasts_json, f) + + def are_there_any_downloads(self): + """ Check if there are any downloaded podcasts episodes + + :return: True - episodes available, False - episodes unavailable + """ + podcast_folder = self.config[PODCASTS_FOLDER] + + if len(podcast_folder.strip()) == 0 or not self.is_podcast_folder_available(): + return False + + if [f for f in os.listdir(podcast_folder) if f.lower().endswith(FILE_PODCASTS_JSON)] == []: + return False + else: + return True + + def is_podcast_folder_available(self): + """ Check if there is podcasts folder available + + :return: True - folder available, False - unavailable + """ + podcast_folder = self.config[PODCASTS_FOLDER] + + if len(podcast_folder.strip()) == 0: + return False + + try: + os.listdir(podcast_folder) + return True + except: + return False diff --git a/util/util.py b/util/util.py index f5f8ff20..b6324f87 100644 --- a/util/util.py +++ b/util/util.py @@ -32,7 +32,7 @@ LANGUAGE, FILE_PLAYBACK, NAME, KEY_SCREENSAVER_DELAY_1, KEY_SCREENSAVER_DELAY_3, KEY_SCREENSAVER_DELAY_OFF, \ FOLDER_LANGUAGES, FILE_FLAG, FOLDER_RADIO_STATIONS, UTF8, FILE_VOICE_COMMANDS, SCREENSAVER_MENU, USE_WEB, \ FILE_WEATHER_CONFIG, EQUALIZER, SCREEN_INFO, WIDTH, HEIGHT, COLOR_BRIGHT, COLOR_CONTRAST, COLOR_DARK_LIGHT, \ - COLOR_MUTE + COLOR_MUTE, SCRIPTS, FILE_BROWSER, FOLDER_IMAGE_SCALE_RATIO, HIDE_FOLDER_NAME from util.keys import * from util.fileutil import FileUtil, FOLDER, FOLDER_WITH_ICON, FILE_AUDIO, FILE_PLAYLIST, FILE_IMAGE, FILE_CD_DRIVE from urllib import request @@ -152,6 +152,7 @@ def __init__(self, connected_to_internet): self.COLOR_MUTE = self.color_to_hex(self.config[COLORS][COLOR_MUTE]) if (not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None)): ssl._create_default_https_context = ssl._create_unverified_context + self.podcasts_util = None def get_labels(self): """ Read labels for current language @@ -1000,6 +1001,20 @@ def load_screensaver(self, name): self.screensaver_cache[name] = s return s + def run_script(self, script_name): + """ Load and run script + + :param script_name: script name + """ + n = script_name[0:script_name.find(".py")] + m = importlib.import_module(SCRIPTS + "." + n) + s = getattr(m, n.title())() + + if s.type == "sync": + s.start() + else: + s.start_thread() + def get_file_icon(self, file_type, file_image_path=None, icon_bb=None, scale_factor=0.6): """ Load image representing file. Five types of icons supported: 1. Folder icon @@ -1031,7 +1046,11 @@ def get_file_icon(self, file_type, file_image_path=None, icon_bb=None, scale_fac elif file_type == FILE_PLAYLIST: return icon_file_playlist elif file_type == FILE_CD_DRIVE: return icon_cd_drive elif file_type == FOLDER_WITH_ICON or file_type == FILE_IMAGE: - bb = (icon_bb[0] * 0.8, icon_bb[1] * 0.8) + scale_ratio = self.config[FOLDER_IMAGE_SCALE_RATIO] + if self.config[HIDE_FOLDER_NAME]: + bb = (icon_bb[0], ((icon_bb[1] * (1/0.7)) * scale_ratio) - 1) + else: + bb = (icon_bb[0] * scale_ratio, icon_bb[1] * scale_ratio) img = self.load_image(file_image_path, bounding_box=bb) if img: return img @@ -1060,10 +1079,14 @@ def load_folder_content(self, folder_name, rows, cols, bounding_box): for index, s in enumerate(content): s.index = index s.name = s.file_name - s.l_name = s.name + s.l_name = s.name s.icon_base = self.get_file_icon(s.file_type, getattr(s, "file_image_path", ""), (item_width, item_height)) s.comparator_item = index s.bgr = self.config[COLORS][COLOR_DARK] + + if (s.file_type == FOLDER_WITH_ICON or s.file_type == FILE_IMAGE) and self.config[HIDE_FOLDER_NAME]: + s.show_label = False + s.show_bgr = True s.index_in_page = index % items_per_page items.append(s) @@ -1204,7 +1227,7 @@ def get_cd_album_art(self, album, bb): self.image_cache[url] = img return (url, img) - + def get_dictionary_value(self, d, key, df=None): """ Return value retrieved from provided dictionary by provided key @@ -1219,7 +1242,7 @@ def get_dictionary_value(self, d, key, df=None): except: return df - def load_image_from_url(self, url): + def load_image_from_url(self, url, header=False): """ Load image from specified URL :param url: image url @@ -1227,11 +1250,18 @@ def load_image_from_url(self, url): :return: image from url """ try: - stream = urlopen(url).read() + if header == False: + stream = urlopen(url).read() + else: + hdrs = {'User-Agent': 'PeppyPlayer +https://github.com/project-owner/Peppy'} + req = request.Request(url, headers=hdrs) + stream = urlopen(req).read() + buf = BytesIO(stream) image = pygame.image.load(buf).convert_alpha() return (url, image) - except: + except Exception as e: + logging.debug(e) return None def get_hash(self, s): @@ -1330,9 +1360,17 @@ def read_storage(self): """ Read storage """ b64 = ZipFile("storage", "r").open("storage.txt").readline() - storage = base64.b64decode(b64)[::-1].decode("utf-8")[2:82] - self.k1 = storage[:40] - self.k2 = storage[40:40+32] + storage = base64.b64decode(b64)[::-1].decode("utf-8")[2:182] + n1 = 40 + n2 = 32 + n3 = 8 + n4 = 60 + n5 = 40 + self.k1 = storage[:n1] + self.k2 = storage[n1 : n1 + n2] + self.k3 = storage[n1 + n2 : n1 + n2 + n3] + self.k4 = storage[n1 + n2 + n3 : n1 + n2 + n3 + n4] + self.k5 = storage[n1 + n2 + n3 + n4 : n1 + n2 + n3 + n4 + n5] def get_flipclock_digits(self, bb): """ Get digits for the flip clock @@ -1367,7 +1405,7 @@ def get_flipclock_separator(self, height): r = self.get_scale_ratio((height, height), image[1], True) i = self.scale_image(image, r) return (path, i) - + def get_flipclock_key(self, image_name, height): """ Get key image for flip clock @@ -1386,4 +1424,13 @@ def get_flipclock_key(self, image_name, height): r = self.get_scale_ratio((w, h), image[1], True) i = self.scale_image(image, r) return (path, i) - \ No newline at end of file + + def get_podcasts_util(self): + """ Get podcasts util object + + :return: podcasts util object + """ + if self.podcasts_util == None: + from util.podcastsutil import PodcastsUtil + self.podcasts_util = PodcastsUtil(self) + return self.podcasts_util diff --git a/web/client/__init__.py b/web/client/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/client/controller.js b/web/client/controller.js index ed9e63e6..56a2de92 100644 --- a/web/client/controller.js +++ b/web/client/controller.js @@ -90,9 +90,6 @@ function dispatchMessageFromServer(msg) { else if(c == "stop_timer") { stopTimer(); } - else if(c == "start_timer") { - startTimer(); - } } /** diff --git a/web/client/uifactory.js b/web/client/uifactory.js index c0f6192d..50453ce8 100644 --- a/web/client/uifactory.js +++ b/web/client/uifactory.js @@ -25,7 +25,6 @@ var XLINK_URL = 'http://www.w3.org/1999/xlink'; var NS_URL = "http://www.w3.org/XML/1998/namespace"; var SCROLL_TIME = "18s"; -var timerRunning = false; var currentTrackTimer = null; var trackTime = 0; var knobStep = 0; @@ -461,18 +460,10 @@ function updateCurrentTrackTimer() { * Set timerRunning flag to false */ function stopTimer() { - timerRunning = false; + stopCurrentTrackTimer(); console.log('stopping timer'); } -/** -* Set timerRunning flag to true -*/ -function startTimer() { - timerRunning = true; - console.log('starting timer'); -} - /** * Stop track timer */ diff --git a/web/server/jsonfactory.py b/web/server/jsonfactory.py index 3101216f..35e83ecd 100644 --- a/web/server/jsonfactory.py +++ b/web/server/jsonfactory.py @@ -19,10 +19,11 @@ from ui.component import Component from ui.container import Container -from util.keys import KEY_PLAY_FILE, KEY_STATIONS, KEY_PLAY_SITE, KEY_CD_TRACKS +from util.keys import KEY_PLAY_FILE, KEY_STATIONS, KEY_PLAY_SITE, KEY_CD_TRACKS, KEY_PODCAST_EPISODES, \ + KEY_PODCAST_PLAYER from util.config import USAGE, USE_BROWSER_STREAM_PLAYER, SCREEN_INFO, VOLUME, MUTE, PAUSE, \ WIDTH, HEIGHT, STREAM_SERVER, STREAM_SERVER_PORT, COLORS, COLOR_WEB_BGR, PLAYER_SETTINGS, \ - AUDIO_FILES + AUDIO_FILES, PODCASTS class JsonFactory(object): """ Converts screen components into Json objects """ @@ -59,7 +60,7 @@ def screen_to_json(self, screen_name, screen): p["bgr"] = p["fgr"] = self.util.color_to_hex((0, 0, 0)) components.append(p) - screens_with_animated_titles = [KEY_STATIONS, KEY_PLAY_FILE, AUDIO_FILES, KEY_PLAY_SITE, KEY_CD_TRACKS] + screens_with_animated_titles = [KEY_STATIONS, KEY_PLAY_FILE, AUDIO_FILES, KEY_PLAY_SITE, KEY_CD_TRACKS, PODCASTS, KEY_PODCAST_EPISODES, KEY_PODCAST_PLAYER] if screen_name in screens_with_animated_titles: components.extend(self.get_title_menu_screen_components(screen)) else: diff --git a/web/server/webserver.py b/web/server/webserver.py index 8237bc0f..fbb6c038 100644 --- a/web/server/webserver.py +++ b/web/server/webserver.py @@ -149,7 +149,7 @@ def title_to_json(self, title): def send_json_to_web_ui(self, j): """ Send provided Json object to all web clients - "param j" Json object to send + "param j": Json object to send """ for c in self.web_clients: e = json.dumps(j).encode(encoding="utf-8")