1# This program is free software; you can redistribute it and/or modify
2# it under the terms of the GNU General Public License as published by
3# the Free Software Foundation; either version 2 of the License, or
4# (at your option) any later version.
5
6from gi.repository import Gtk
7from quodlibet import _, app
8from quodlibet.plugins import PluginConfigMixin
9from quodlibet.plugins.songshelpers import has_bookmark
10from quodlibet.plugins.events import EventPlugin
11from quodlibet.qltk import Icons
12from quodlibet.qltk.entry import UndoEntry
13from quodlibet.qltk.tracker import TimeTracker
14
15
16class SeekPointsPlugin(EventPlugin, PluginConfigMixin):
17    """The plugin class."""
18
19    PLUGIN_ID = "Seekpoints"
20    PLUGIN_NAME = _("Seekpoint Bookmarks")
21    PLUGIN_ICON = Icons.GO_JUMP
22    PLUGIN_CONFIG_SECTION = __name__
23    PLUGIN_DESC = _(
24       "Store Seekpoints A and/or B for tracks. "
25       "Skip to time A and stop after time B when track is played.\n"
26       "Note that changing the names of the points below does not "
27       "update the actual bookmark names, it only changes which "
28       "bookmark names the plugin looks for when deciding whether to seek.")
29
30    CFG_SEEKPOINT_A_TEXT = "A"
31    CFG_SEEKPOINT_B_TEXT = "B"
32    DEFAULT_A_TEXT = "A"
33    DEFAULT_B_TEXT = "B"
34
35    def enabled(self):
36        self._seekpoint_A, self._seekpoint_B = self._get_seekpoints()
37        self._tracker = TimeTracker(app.player)
38        self._tracker.connect('tick', self._on_tick)
39
40    def disabled(self):
41        self._tracker.destroy()
42
43    def plugin_on_song_started(self, song):
44        """Seeks to point A if it exists, and also fetches the bookmarks of the
45           current track that matches the seekpoint names set in config.
46        """
47        self._seekpoint_A, self._seekpoint_B = self._get_seekpoints()
48        if not self._seekpoint_A:
49            return
50        self._seek(self._seekpoint_A)
51
52    def _on_tick(self, tracker):
53        """Checks whether the current position is past point B if it exists,
54           and if it is -- seek to the end of the track.
55        """
56        if not self._seekpoint_B:
57            return
58
59        time = app.player.get_position() // 1000
60        if self._seekpoint_B <= time:
61            self._seek(app.player.info("~#length"))
62
63    def _get_seekpoints(self):
64        """Reads seekpoint-names from config, which are compared to the
65           bookmark-names of the current track to get timestamps (if any).
66        """
67        if not app.player.song:
68            return None, None
69
70        marks = []
71        if has_bookmark(app.player.song):
72            marks = app.player.song.bookmarks
73
74        seekpoint_A = None
75        seekpoint_B = None
76        seekpoint_A_name = self.config_get(self.CFG_SEEKPOINT_A_TEXT,
77                                           self.DEFAULT_A_TEXT)
78        seekpoint_B_name = self.config_get(self.CFG_SEEKPOINT_B_TEXT,
79                                           self.DEFAULT_B_TEXT)
80        for time, mark in marks:
81            if mark == seekpoint_A_name:
82                seekpoint_A = time
83            elif mark == seekpoint_B_name:
84                seekpoint_B = time
85
86        # if seekpoints are not properly ordered (or identical), the track
87        # will likely endlessly seek when looping tracks, so discard B
88        # (maybe raise an exception for the plugin list?).
89        if (seekpoint_A is not None) and (seekpoint_B is not None):
90            if seekpoint_A >= seekpoint_B:
91                return seekpoint_A, None
92
93        return seekpoint_A, seekpoint_B
94
95    def _seek(self, seconds):
96        app.player.seek(seconds * 1000)
97
98    def PluginPreferences(self, parent):
99        vb = Gtk.VBox(spacing=12)
100
101        # Bookmark name to use for point A
102        hb = Gtk.HBox(spacing=6)
103        entry = UndoEntry()
104        entry.set_text(self.config_get(self.CFG_SEEKPOINT_A_TEXT,
105                                       self.DEFAULT_A_TEXT))
106        entry.connect('changed', self.config_entry_changed,
107                      self.CFG_SEEKPOINT_A_TEXT)
108        lbl = Gtk.Label(label=_("Bookmark name for point A"))
109        entry.set_tooltip_markup(_("Bookmark name to check for when "
110            "a track is started, and if found the player seeks to that "
111            "timestamp"))
112        lbl.set_mnemonic_widget(entry)
113        hb.pack_start(lbl, False, True, 0)
114        hb.pack_start(entry, True, True, 0)
115        vb.pack_start(hb, True, True, 0)
116
117        # Bookmark name to use for point B
118        hb = Gtk.HBox(spacing=6)
119        entry = UndoEntry()
120        entry.set_text(self.config_get(self.CFG_SEEKPOINT_B_TEXT,
121                                       self.DEFAULT_B_TEXT))
122        entry.connect('changed', self.config_entry_changed,
123                      self.CFG_SEEKPOINT_B_TEXT)
124        lbl = Gtk.Label(label=_("Bookmark name for point B"))
125        entry.set_tooltip_markup(_("Bookmark name to use each tick during "
126            "play of a track if it exist. If the current position exceeds "
127            "the timestamp, seek to the end of the track."))
128        lbl.set_mnemonic_widget(entry)
129        hb.pack_start(lbl, False, True, 0)
130        hb.pack_start(entry, True, True, 0)
131        vb.pack_start(hb, True, True, 0)
132
133        return vb
134