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")