def __set_play_status_updater(self, enable): """ Starts/stops the play status ui update timer. Restarts if enable is True and the timer is already running. :params enable: Boolean """ if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None if enable and self.ui.is_playing and self.play_status_updater is None: self.play_status_updater = IntervalTimer(1, self.__update_time) self.play_status_updater.start()
def __init__(self): super().__init__() self._first_play = True self._gst_player: player = player.get_playbin() player.add_player_listener(self._pass_legacy_player_events) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick)
def __init__(self): super().__init__() self._gst_player: player = player.get_playbin() player.add_player_listener(self._pass_legacy_player_events) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick) self._fadeout_thread: Optional[Thread] = None player.init() last_book = self._library.last_played_book if last_book: last_book.last_played = int(time.time()) player.load_file(last_book.current_chapter._db_object) self._rewind_feature()
def __init__(self): super().__init__() self._book: Optional[Book] = None self._play_next_chapter: bool = True self._gst_player.add_listener(self._on_gst_player_event) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick) self._fadeout_thread: Optional[Thread] = None self._gst_player.init() self._load_last_book()
class Titlebar: """ This class contains all titlebar logic. """ # main ui class ui = None # Titlebar timer for ui updates on position play_status_updater = None # Is the mouse button currently down on the progress scale? progress_scale_clicked = False current_book = None # Remaining time for this book in seconds # This doesn't include the current track # and will only be refreshed when the loaded track changes! current_remaining = 0 # Elapsed time for this book in seconds # This doesn't include the current track # and will only be refreshed when the loaded track changes! current_elapsed = 0 def __init__(self): self.ui = cozy.ui.main_view.CozyUI() # init buttons self.play_button = self.ui.get_object("play_button") self.prev_button = self.ui.get_object("prev_button") self.volume_button = self.ui.get_object("volume_button") self.volume_button.set_value( tools.get_glib_settings().get_double("volume")) self.timer_button = self.ui.get_object("timer_button") self.playback_speed_button = self.ui.get_object( "playback_speed_button") self.search_button = self.ui.get_object("search_button") self.menu_button = self.ui.get_object("menu_button") self.remaining_event_box = self.ui.get_object("remaining_event_box") # init labels self.title_label = self.ui.get_object("title_label") self.subtitle_label = self.ui.get_object("subtitle_label") self.current_label = self.ui.get_object("current_label") self.current_label.set_visible(False) self.remaining_label = self.ui.get_object("remaining_label") self.remaining_label.set_visible(False) # init images self.play_img = self.ui.get_object("play_img") self.pause_img = self.ui.get_object("pause_img") self.cover_img = self.ui.get_object("cover_img") self.cover_img_box = self.ui.get_object("cover_img_box") # init progress scale self.progress_scale = self.ui.get_object("progress_scale") self.progress_scale.set_increments(30.0, 60.0) self.progress_scale.set_visible(False) self.status_stack = self.ui.get_object("status_stack") self.status_label = self.ui.get_object("status_label") self.update_progress_bar = self.ui.get_object("update_progress_bar") self.throbber = self.ui.get_object("spinner") self.throbber.set_visible(False) self.progress_bar = self.ui.get_object("progress_bar") self.__init_signals() # elementaryos specific stuff if tools.is_elementary(): self.cover_img_box.props.width_request = 28 self.cover_img_box.props.height_request = 28 self.volume_button.props.relief = Gtk.ReliefStyle.NONE else: self.volume_button.get_style_context().remove_class("flat") # app menu self.menu_builder = Gtk.Builder.new_from_resource( "/de/geigi/cozy/titlebar_menu.ui") menu = self.menu_builder.get_object("titlebar_menu") self.menu_button.set_menu_model(menu) def __init_signals(self): self.play_button.connect("clicked", self.__on_play_pause_clicked) self.prev_button.connect("clicked", self.__on_rewind_clicked) self.volume_button.connect("value-changed", self.__on_volume_changed) self.remaining_event_box.connect("button-release-event", self._on_remaining_clicked) # init progress scale self.progress_scale.connect("value-changed", self.update_ui_time) self.progress_scale.connect("button-release-event", self.__on_progress_clicked) self.progress_scale.connect("button-press-event", self.__on_progress_press) self.progress_scale.connect("key-press-event", self.__on_progress_key_pressed) player.add_player_listener(self.__player_changed) def activate(self): # attach to child event signals self.ui.speed.add_listener(self.__on_playback_speed_changed) # attach popovers self.timer_button.set_popover(self.ui.sleep_timer.get_popover()) self.playback_speed_button.set_popover(self.ui.speed.get_popover()) self.search_button.set_popover(self.ui.search.get_popover()) def block_ui_buttons(self, block, scan=False): """ Block the ui buttons when gui actions are in progress. :param block: Boolean """ sensitive = not block self.play_button.set_sensitive(sensitive) self.volume_button.set_sensitive(sensitive) self.prev_button.set_sensitive(sensitive) self.timer_button.set_sensitive(sensitive) self.playback_speed_button.set_sensitive(sensitive) if scan: self.search_button.set_sensitive(sensitive) def get_ui_buttons_blocked(self): """ Are the UI buttons currently blocked? """ return not self.play_button.get_sensitive() def play(self): """ """ self.play_button.set_image(self.pause_img) self.__set_play_status_updater(True) def pause(self): """ """ self.play_button.set_image(self.play_img) self.__set_play_status_updater(False) def stop(self): """ Remove all information about a playing book from the titlebar. """ self.play_button.set_image(self.play_img) self.__set_play_status_updater(False) self.title_label.set_text("") self.subtitle_label.set_text("") self.cover_img.set_from_pixbuf(None) self.progress_scale.set_range(0, 0) self.progress_scale.set_visible(False) self.progress_scale.set_sensitive(False) self.remaining_label.set_visible(False) self.current_label.set_visible(False) self.block_ui_buttons(True) def set_title_cover(self, pixbuf, size): """ Sets the cover in the title bar. """ if pixbuf: surface = Gdk.cairo_surface_create_from_pixbuf( pixbuf, self.ui.window.get_scale_factor(), None) self.cover_img.set_from_surface(surface) self.cover_img.set_tooltip_text( player.get_current_track().book.name) else: self.cover_img.set_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.MENU) self.cover_img.props.pixel_size = size def set_progress_scale_width(self, width): self.progress_scale.props.width_request = width def update_ui_time(self, widget): """ Displays the value of the progress slider in the text boxes as time. """ val = int(self.progress_scale.get_value()) if tools.get_glib_settings().get_boolean("titlebar-remaining-time"): label_text = tools.seconds_to_str(val, display_zero_h=True) else: label_text = tools.seconds_to_str(val) self.current_label.set_markup("<tt><b>" + label_text + "</b></tt>") track = player.get_current_track() if track: if tools.get_glib_settings().get_boolean( "titlebar-remaining-time"): total = self.progress_scale.get_adjustment().get_upper() remaining_secs: int = int((total - val)) self.remaining_label.set_markup( "<tt><b>-" + tools.seconds_to_str(remaining_secs, display_zero_h=True) + "</b></tt>") else: remaining_secs: int = int((track.length / self.ui.speed.get_speed()) - val) self.remaining_label.set_markup( "<tt><b>-" + tools.seconds_to_str( remaining_secs, display_zero_h=False) + "</b></tt>") if self.ui.book_overview.book and self.current_book.id == self.ui.book_overview.book.id: self.ui.book_overview.update_time() def update_track_ui(self): # set data of new stream in ui track = player.get_current_track() if track is None: return self.title_label.set_text(track.book.name) self.subtitle_label.set_text(track.name) self.block_ui_buttons(False) self.progress_scale.set_sensitive(True) self.progress_scale.set_visible(True) # only change cover when book has changed if self.current_book is not track.book: self.current_book = track.book if tools.is_elementary(): size = 28 else: size = 40 self.set_title_cover( artwork_cache.get_cover_pixbuf( track.book, self.ui.window.get_scale_factor(), size), size) self.current_remaining = get_book_remaining(self.current_book, False) self.current_elapsed = get_book_progress(self.current_book, False) self.__update_progress_scale_range() if tools.get_glib_settings().get_boolean("titlebar-remaining-time"): self.progress_scale.set_value(self.current_elapsed / self.ui.speed.get_speed()) else: self.progress_scale.set_value(0) self.update_ui_time(None) self.current_label.set_visible(True) self.remaining_label.set_visible(True) def switch_to_working(self, message, first): """ Switch the UI state to working. This is used for example when an import is currently happening. This blocks the user from doing some stuff like starting playback. """ self.throbber.set_visible(True) self.throbber.start() self.status_label.set_text(message) if not first: self.status_stack.props.visible_child_name = "working" def switch_to_playing(self): """ Switch the UI state back to playing. This enables all UI functionality for the user. """ self.status_stack.props.visible_child_name = "playback" self.throbber.stop() self.throbber.set_visible(False) def load_last_book(self): if Settings.get().last_played_book: self.update_track_ui() self.update_ui_time(self.progress_scale) cur_m, cur_s = player.get_current_duration_ui() self.__set_progress_scale_value(cur_m * 60 + cur_s) pos = int(player.get_current_track().position) if tools.get_glib_settings().get_boolean("replay"): log.info("Replaying the previous 30 seconds.") amount = 30 * 1000000000 if (pos < amount): pos = 0 else: pos = pos - amount self.__set_progress_scale_value( int(pos / 1000000000 / self.ui.speed.get_speed())) def _on_remaining_clicked(self, widget, sender): """ Switch between displaying the time for a track or the whole book. """ if widget.get_name is not "titlebar_remaining_time_eventbox": if tools.get_glib_settings().get_boolean( "titlebar-remaining-time"): tools.get_glib_settings().set_boolean( "titlebar-remaining-time", False) else: tools.get_glib_settings().set_boolean( "titlebar-remaining-time", True) self._on_progress_setting_changed() return True def _on_progress_setting_changed(self): self.__update_time() self.__update_progress_scale_range() self.update_ui_time(None) def __on_play_pause_clicked(self, button): """ Play/Pause the player. """ player.play_pause(None) pos = self.ui.get_playback_start_position() player.jump_to_ns(pos) def __on_rewind_clicked(self, button): """ """ seconds = 30 * self.ui.speed.get_speed() if self.ui.first_play: ns = seconds * 1000000000 track = player.get_current_track() pos = track.position if (pos > ns): pos -= ns else: pos = 0 player.save_current_track_position(pos=pos, track=track) else: player.rewind(seconds) # we want to see the jump imediatly therefore we apply the new time manually if self.progress_scale.get_value() > 30: self.progress_scale.set_value(self.progress_scale.get_value() - 30) else: self.progress_scale.set_value(0) def __on_volume_changed(self, widget, value): """ Sets the ui value in the player. """ player.set_volume(value) tools.get_glib_settings().set_double("volume", value) def __on_progress_press(self, widget, sender): """ Remember that progress scale is clicked so it won't get updates from the player. """ self.progress_scale_clicked = True # If the user drags the slider we don't want to jump back # another 30 seconds on first play if self.ui.first_play: self.ui.first_play = False return False def __on_progress_clicked(self, widget, sender): """ Jump to the slided time and release the progress scale update lock. """ value = self.progress_scale.get_value() * self.ui.speed.get_speed() if tools.get_glib_settings().get_boolean("titlebar-remaining-time"): track, time = get_track_from_book_time(self.current_book, value) if track.id == player.get_current_track().id: player.jump_to(time) else: player.load_file(track) player.play_pause(None, True) self.__set_progress_scale_value(time / self.ui.speed.get_speed()) player.jump_to(time) else: player.jump_to(value) self.progress_scale_clicked = False return False def __on_progress_key_pressed(self, widget, event): """ Jump to the modified time. """ old_val = self.progress_scale.get_value() if event.keyval == Gdk.KEY_Up or event.keyval == Gdk.KEY_Left: if old_val > 30.0: player.jump_to(old_val - 30) else: player.jump_to(0) elif event.keyval == Gdk.KEY_Down or event.keyval == Gdk.KEY_Right: upper = self.progress_scale.get_adjustment().get_upper() if old_val + 30.0 < upper: player.jump_to(old_val + 30) else: player.jump_to(upper) return False def __update_progress_scale_range(self): """ Update the progress scale range including the current playback speed. """ if tools.get_glib_settings().get_boolean("titlebar-remaining-time"): total = get_book_duration( self.current_book) / self.ui.speed.get_speed() else: total = player.get_current_track( ).length / self.ui.speed.get_speed() self.progress_scale.set_range(0, total) def __set_progress_scale_value(self, value): """ Set a given progress scale value. :param value: This value already needs playback speed compensation. """ if tools.get_glib_settings().get_boolean("titlebar-remaining-time"): value += (self.current_elapsed / self.ui.speed.get_speed()) self.progress_scale.set_value(value) def __set_play_status_updater(self, enable): """ Starts/stops the play status ui update timer. Restarts if enable is True and the timer is already running. :params enable: Boolean """ if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None if enable and self.ui.is_playing and self.play_status_updater is None: self.play_status_updater = IntervalTimer(1, self.__update_time) self.play_status_updater.start() def __update_time(self): """ Update the current and remaining time. """ if not self.progress_scale_clicked: cur_m, cur_s = player.get_current_duration_ui() Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.__set_progress_scale_value, (cur_m * 60 + cur_s)) def __on_playback_speed_changed(self, event, message): """ Handler for events that occur the playback speed object. """ if event == "playback-speed-changed": speed = message m, s = player.get_current_duration_ui() value = 60 * m + s self.__update_progress_scale_range() self.__set_progress_scale_value(value) self.update_ui_time(None) def __player_changed(self, event, message): """ Listen to and handle all gst player messages that are important for the ui. """ if event == "track-changed": self.update_track_ui() def close(self): log.info("Closing.") if self.play_status_updater: self.play_status_updater.stop()
class Player(EventSender): _library: Library = inject.attr(Library) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) def __init__(self): super().__init__() self._gst_player: player = player.get_playbin() player.add_player_listener(self._pass_legacy_player_events) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick) self._fadeout_thread: Optional[Thread] = None player.init() last_book = self._library.last_played_book if last_book: last_book.last_played = int(time.time()) player.load_file(last_book.current_chapter._db_object) self._rewind_feature() @property def loaded_book(self) -> Optional[Book]: current_track = player.get_current_track() if not current_track: return None track_id = current_track.id book = None for b in self._library.books: if any(chapter.id == track_id for chapter in b.chapters): book = b break return book @property def loaded_chapter(self) -> Optional[Chapter]: current_track = player.get_current_track() if not current_track: return None chapter = None for c in self._library.chapters: if c.id == current_track.id: chapter = c return chapter @property def playing(self) -> bool: return player.is_playing() @property def position(self) -> int: return player.get_current_duration() @position.setter def position(self, new_value: int): player.jump_to(new_value) @property def volume(self) -> float: return player.get_volume() @volume.setter def volume(self, new_value: float): player.set_volume(new_value) self._app_settings.volume = new_value @property def play_next_chapter(self) -> bool: return player.get_play_next() @play_next_chapter.setter def play_next_chapter(self, value: bool): player.set_play_next(value) def play_pause(self): player.play_pause(None) def play_pause_book(self, book: Book): if not book: log.error("Cannot play book which is None.") reporter.error("player", "Cannot play book which is None.") return self._play_chapter(book, book.current_chapter) def play_pause_chapter(self, book: Book, chapter: Chapter): if not book or not chapter: log.error("Cannot play chapter which is None.") reporter.error("player", "Cannot play chapter which is None.") return self._play_chapter(book, chapter) book.position = chapter.id def _play_chapter(self, book: Book, chapter: Chapter): current_track = player.get_current_track() book.last_played = int(time.time()) if current_track and current_track.file == chapter.file: player.play_pause(None) else: player.load_file(chapter._db_object) player.play_pause(None, True) def rewind(self): if self.loaded_book: player.rewind(30 / self.loaded_book.playback_speed) def destroy(self): if self._fadeout_thread: self._fadeout_thread.stop() def _rewind_feature(self): if self._app_settings.replay: player.rewind(30 / self.loaded_book.playback_speed) def _pass_legacy_player_events(self, event, message): if event == "play": self._start_tick_thread() elif event == "stop" or event == "pause" or event == "closing": self._stop_tick_thread() if (event == "play" or event == "pause") and message: message = message.id # this is evil and will be removed when the old player is replaced if event == "track-changed": book = self.loaded_book if book and message: book.position = message.id self.volume = self._app_settings.volume player.set_playback_speed(book.playback_speed) message = book if event == "book-finished": book = self.loaded_book if book: book.position = -1 self.emit_event(event, message) def _start_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = IntervalTimer(1, self._emit_tick) self.play_status_updater.start() def _stop_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None def _emit_tick(self): if self.loaded_chapter: self.loaded_chapter.position = self.position self.emit_event_main_thread("position", self.position) def pause(self, fadeout: bool = False): if fadeout and not self._fadeout_thread: log.info("Starting fadeout playback") self._fadeout_thread = Thread(target=self._fadeout_playback, name="PlayerFadeoutThread") self._fadeout_thread.start() return if self.playing: self.play_pause() def _fadeout_playback(self): duration = self._app_settings.sleep_timer_fadeout_duration * 20 current_vol = player.get_volume() for i in range(0, duration): player.set_volume(max(current_vol - (i / duration), 0)) time.sleep(0.05) log.info("Fadeout completed.") self.play_pause() player.set_volume(current_vol) self.emit_event("fadeout-finished", None) self._fadeout_thread = None
def _start_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = IntervalTimer(1, self._emit_tick) self.play_status_updater.start()
class Player(EventSender): _library: Library = inject.attr(Library) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) _offline_cache: OfflineCache = inject.attr(OfflineCache) _info_bar: InfoBanner = inject.attr(InfoBanner) _gst_player: GstPlayer = inject.attr(GstPlayer) def __init__(self): super().__init__() self._book: Optional[Book] = None self._play_next_chapter: bool = True self._gst_player.add_listener(self._on_gst_player_event) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick) self._fadeout_thread: Optional[Thread] = None self._gst_player.init() self._load_last_book() def _load_last_book(self): last_book = self._library.last_played_book if last_book: self._continue_book(last_book) self._rewind_feature() @property def loaded_book(self) -> Optional[Book]: return self._book @property def loaded_chapter(self) -> Optional[Chapter]: if self._book: return self._book.current_chapter else: return None @property def playing(self) -> bool: return self._gst_player.state == GstPlayerState.PLAYING @property def position(self) -> int: return self._gst_player.position @position.setter def position(self, new_value: int): self._gst_player.position = self.loaded_chapter.start_position + ( new_value * NS_TO_SEC) @property def volume(self) -> float: return self._gst_player.volume @volume.setter def volume(self, new_value: float): self._gst_player.volume = new_value self._app_settings.volume = new_value @property def play_next_chapter(self) -> bool: return self._play_next_chapter @play_next_chapter.setter def play_next_chapter(self, value: bool): self._play_next_chapter = value @property def playback_speed(self) -> float: return self._gst_player.playback_speed @playback_speed.setter def playback_speed(self, value: float): self._gst_player.playback_speed = value def play_pause(self): if self._gst_player.state == GstPlayerState.PAUSED: self._gst_player.play() elif self._gst_player.state == GstPlayerState.PLAYING: self._gst_player.pause() else: log.error("Trying to play/pause although player is in STOP state.") reporter.error( "player", "Trying to play/pause although player is in STOP state.") def pause(self, fadeout: bool = False): if fadeout and not self._fadeout_thread: log.info("Starting fadeout playback") self._fadeout_thread = Thread(target=self._fadeout_playback, name="PlayerFadeoutThread") self._fadeout_thread.start() return if self._gst_player.state == GstPlayerState.PLAYING: self._gst_player.pause() def play_pause_book(self, book: Book): if not book: log.error("Cannot play book which is None.") reporter.error("player", "Cannot play book which is None.") return if self._book == book: self.play_pause() else: self._continue_book(book) self._gst_player.play() def play_pause_chapter(self, book: Book, chapter: Chapter): if not book or not chapter: log.error("Cannot play chapter which is None.") reporter.error("player", "Cannot play chapter which is None.") return if self._book and self._book.current_chapter == chapter: self.play_pause() return if self._book != book: self._load_book(book) self._load_chapter(chapter) self._gst_player.play() book.position = chapter.id def rewind(self): state = self._gst_player.state if state != GstPlayerState.STOPPED: self._rewind_in_book() if state == GstPlayerState.PLAYING: self._gst_player.play() def forward(self): state = self._gst_player.state if state != GstPlayerState.STOPPED: self._forward_in_book() if state == GstPlayerState.PLAYING: self._gst_player.play() def destroy(self): self._gst_player.dispose() self._stop_tick_thread() if self._fadeout_thread: self._fadeout_thread.stop() def _load_book(self, book: Book): if self._book == book: log.info("Not loading new book because it's unchanged.") return self._book = book self._book.last_played = int(time.time()) def _continue_book(self, book: Book): if self._book == book: log.info("Not loading new book because it's unchanged.") return self._load_book(book) self._load_chapter(book.current_chapter) def _load_chapter(self, chapter: Chapter): file_changed = False if not self._book: log.error("There is no book loaded but there should be.") reporter.error("player", "There is no book loaded but there should be.") return self._library.last_played_book = self._book media_file_path = self._get_playback_path(chapter) if self._gst_player.loaded_file_path == media_file_path: log.info( "Not loading a new file because the new chapter is within the old file." ) else: log.info("Loading new file for chapter.") try: self._gst_player.load_file(media_file_path) file_changed = True except FileNotFoundError: self._handle_file_not_found() return if file_changed or self._should_jump_to_chapter_position( chapter.position): self._gst_player.position = chapter.position self._gst_player.playback_speed = self._book.playback_speed if file_changed or self._book.position != chapter.id: self._book.position = chapter.id self.emit_event_main_thread("chapter-changed", self._book) def _get_playback_path(self, chapter: Chapter): if self._book.offline and self._book.downloaded: path = self._offline_cache.get_cached_path(chapter) if path and os.path.exists(path): return path return chapter.file def _rewind_in_book(self): if not self._book: log.error("Rewind in book not possible because no book is loaded.") reporter.error( "player", "Rewind in book not possible because no book is loaded.") return current_position = self._gst_player.position current_position_relative = max( current_position - self.loaded_chapter.start_position, 0) chapter_number = self._book.chapters.index(self._book.current_chapter) rewind_seconds = self._app_settings.rewind_duration * self.playback_speed if current_position_relative / NS_TO_SEC - rewind_seconds > 0: self._gst_player.position = current_position - NS_TO_SEC * rewind_seconds elif chapter_number > 0: previous_chapter = self._book.chapters[chapter_number - 1] self._load_chapter(previous_chapter) self._gst_player.position = previous_chapter.end_position + ( current_position_relative - NS_TO_SEC * rewind_seconds) else: self._gst_player.position = 0 def _forward_in_book(self): if not self._book: log.error( "Forward in book not possible because no book is loaded.") reporter.error( "player", "Forward in book not possible because no book is loaded.") return current_position = self._gst_player.position current_position_relative = max( current_position - self.loaded_chapter.start_position, 0) old_chapter = self._book.current_chapter chapter_number = self._book.chapters.index(self._book.current_chapter) forward_seconds = self._app_settings.forward_duration * self.playback_speed if current_position_relative / NS_TO_SEC + forward_seconds < self._book.current_chapter.length: self._gst_player.position = current_position + (NS_TO_SEC * forward_seconds) elif chapter_number < len(self._book.chapters) - 1: next_chapter = self._book.chapters[chapter_number + 1] self._load_chapter(next_chapter) self._gst_player.position = next_chapter.start_position + ( NS_TO_SEC * forward_seconds - (old_chapter.length * NS_TO_SEC - current_position_relative)) else: self._next_chapter() def _rewind_feature(self): if self._app_settings.replay: self._rewind_in_book() self._emit_tick() def _next_chapter(self): if not self._book: log.error( "Cannot play next chapter because no book reference is stored." ) reporter.error( "player", "Cannot play next chapter because no book reference is stored." ) index_current_chapter = self._book.chapters.index( self._book.current_chapter) self._book.current_chapter.position = self._book.current_chapter.start_position if len(self._book.chapters) <= index_current_chapter + 1: log.info("Book finished, stopping playback.") self._finish_book() self._gst_player.stop() else: chapter = self._book.chapters[index_current_chapter + 1] chapter.position = chapter.start_position self.play_pause_chapter(self._book, chapter) def _on_gst_player_event(self, event: str, message): if event == "file-finished": self._next_chapter() elif event == "resource-not-found": self._handle_file_not_found() elif event == "state" and message == GstPlayerState.PLAYING: self._book.last_played = int(time.time()) self._start_tick_thread() self.emit_event_main_thread("play", self._book) elif event == "state" and message == GstPlayerState.PAUSED: self._stop_tick_thread() self.emit_event_main_thread("pause") elif event == "state" and message == GstPlayerState.STOPPED: self._stop_playback() elif event == "error": self._handle_gst_error(message) def _handle_gst_error(self, error: GLib.Error): if error.code != Gst.ResourceError.BUSY: self._info_bar.show(error.message) if error.code == Gst.ResourceError.OPEN_READ or Gst.ResourceError.READ: self._stop_playback() def _handle_file_not_found(self): if self.loaded_chapter: FileNotFoundDialog(self.loaded_chapter).show() self._stop_playback() else: log.warning( "No chapter loaded, cannot display file not found dialog.") def _stop_playback(self): self._stop_tick_thread() self._book = None self.emit_event_main_thread("pause") self.emit_event_main_thread("stop") def _finish_book(self): if self._book: self._book.position = -1 self._library.last_played_book = None self.emit_event_main_thread("book-finished", self._book) def _start_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = IntervalTimer(1, self._emit_tick) self.play_status_updater.start() def _stop_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None def _emit_tick(self): if not self.loaded_chapter or not self.loaded_book: log.info("Not emitting tick because no book/chapter is loaded.") return if self.position > self.loaded_chapter.end_position: self._next_chapter() self.loaded_chapter.position = self.position position_for_ui = self.position - self.loaded_chapter.start_position self.emit_event_main_thread("position", position_for_ui) def _fadeout_playback(self): duration = self._app_settings.sleep_timer_fadeout_duration * 20 current_vol = self._gst_player.volume for i in range(0, duration): volume = max(current_vol - (i / duration), 0) self._gst_player.position = volume time.sleep(0.05) log.info("Fadeout completed.") self.play_pause() self._gst_player.volume = current_vol self.emit_event_main_thread("fadeout-finished", None) self._fadeout_thread = None def _should_jump_to_chapter_position(self, position: int) -> bool: """ Should the player jump to the given position? This allows gapless playback for media files that contain many chapters. """ difference = abs(self.position - position) if difference < 10**9: return False return True
class Player(EventSender): _library: Library = inject.attr(Library) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) def __init__(self): super().__init__() self._first_play = True self._gst_player: player = player.get_playbin() player.add_player_listener(self._pass_legacy_player_events) self.play_status_updater: IntervalTimer = IntervalTimer( 1, self._emit_tick) @property def loaded_book(self) -> Optional[Book]: current_track = player.get_current_track() if not current_track: return None track_id = current_track.id book = None for b in self._library.books: if any(chapter.id == track_id for chapter in b.chapters): book = b break return book @property def loaded_chapter(self) -> Optional[Chapter]: current_track = player.get_current_track() if not current_track: return None chapter = None for c in self._library.chapters: if c.id == current_track.id: chapter = c return chapter @property def playing(self) -> bool: return player.is_playing() @property def position(self) -> int: return player.get_current_duration() def play_pause_book(self, book: Book): if not book: log.error("Cannot play book which is None.") reporter.error("player", "Cannot play book which is None.") return self._play_chapter(book, book.current_chapter) def play_pause_chapter(self, book: Book, chapter: Chapter): if not book or not chapter: log.error("Cannot play chapter which is None.") reporter.error("player", "Cannot play chapter which is None.") return self._play_chapter(book, chapter) book.position = chapter.id def _play_chapter(self, book: Book, chapter: Chapter): current_track = player.get_current_track() book.last_played = int(time.time()) if current_track and current_track.file == chapter.file: player.play_pause(None) else: player.load_file(chapter._db_object) player.play_pause(None, True) def rewind(self): if self.loaded_book: player.rewind(30 / self.loaded_book.playback_speed) def _rewind_feature(self): if self._first_play and self._app_settings.replay: self._first_play = False player.rewind(30 / self.loaded_book.playback_speed) def _pass_legacy_player_events(self, event, message): if event == "play": self._start_tick_thread() elif event == "stop" or event == "pause" or event == "closing": self._stop_tick_thread() if (event == "play" or event == "pause") and message: message = message.id # TODO: This needs to be done when the last chapter is first loaded after startup if self._first_play: self._rewind_feature() # this is evil and will be removed when the old player is replaced if event == "track-changed": book = self.loaded_book if book and message: book.position = message.id if event == "book-finished": book = self.loaded_book if book: book.position = -1 self.emit_event(event, message) def _start_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = IntervalTimer(1, self._emit_tick) self.play_status_updater.start() def _stop_tick_thread(self): if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None def _emit_tick(self): if self.loaded_chapter: self.loaded_chapter.position = self.position self.emit_event_main_thread("position", self.position)