Beispiel #1
0
    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()
Beispiel #2
0
    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)
Beispiel #3
0
    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()
Beispiel #4
0
    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()
Beispiel #5
0
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()
Beispiel #6
0
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
Beispiel #7
0
    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()
Beispiel #8
0
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
Beispiel #9
0
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)