Esempio n. 1
0
def errorhook(exc_info=None):
    """This is the main entry point

    Call in an exception context. Thread safe.

    def my_thread():
        try:
            do_work()
        except Exception:
            errorhook()
    """

    global _error_lock, _errorhook_enabled

    if not _errorhook_enabled:
        return

    if exc_info is None:
        exc_info = sys.exc_info()

    if exc_info[0] is None:
        # called outside of an exception context, just ignore
        print_e("no active exception!")
        return

    # In case something goes wrong during error handling print it first
    print_exc(exc_info)

    if not _error_lock.acquire(False):
        # Make sure only one of these is active at a time
        return

    # write error and logs to disk
    dump_dir = os.path.join(quodlibet.get_user_dir(), "dumps")
    dump_to_disk(dump_dir, exc_info)

    sentry = get_sentry()

    # For crashes the stack trace is not enough to differentiating different
    # crash sources. We need to give our own grouping key (fingerprint) based
    # on the stack trace provided by faulthandler.
    fingerprint = None
    if isinstance(exc_info[1], FaultHandlerCrash):
        fingerprint = ["{{ default }}", exc_info[1].get_grouping_key()]

    try:
        sentry_error = sentry.capture(exc_info, fingerprint=fingerprint)
    except SentryError:
        sentry_error = None

    def called_in_main_thread():
        try:
            run_error_dialogs(exc_info, sentry_error)
        finally:
            _error_lock.release()

    if is_main_thread():
        called_in_main_thread()
    else:
        GLib.idle_add(called_in_main_thread)
Esempio n. 2
0
 def parse_body(self, body, query_path_=None):
     if body is None:
         raise QueryPluginError
     body = body.strip().lower()
     # Use provided query file for testing
     if query_path_:
         query_path = query_path_
     else:
         query_path = os.path.join(get_user_dir(), 'lists', 'queries.saved')
     try:
         with open(query_path, 'rU') as query_file:
             for query_string in query_file:
                 name = next(query_file).strip().lower()
                 if name == body:
                     try:
                         return Query(query_string.strip())
                     except QueryError:
                         raise QueryPluginError
         # We've searched the whole file and haven't found a match
         raise QueryPluginError
     except IOError:
         raise QueryPluginError
     except StopIteration:
         # The file has an odd number of lines. This shouldn't happen unless
         # it has been externally modified
         raise QueryPluginError
Esempio n. 3
0
def init():
    global browsers

    # ignore double init (for the test suite)
    if browsers:
        return

    this_dir = os.path.dirname(__file__)
    load_pyc = os.name == 'nt'
    modules = load_dir_modules(this_dir,
                               package=__package__,
                               load_compiled=load_pyc)

    user_dir = os.path.join(quodlibet.get_user_dir(), "browsers")
    if os.path.isdir(user_dir):
        modules += load_dir_modules(user_dir,
                                    package="quodlibet.fake.browsers",
                                    load_compiled=load_pyc)

    for browser in modules:
        try:
            browsers.extend(browser.browsers)
        except AttributeError:
            print_w("%r doesn't contain any browsers." % browser.__name__)

    def is_browser(Kind):
        return isinstance(Kind, type) and issubclass(Kind, Browser)

    browsers = filter(is_browser, browsers)

    if not browsers:
        raise SystemExit("No browsers found!")

    browsers.sort(key=lambda Kind: Kind.priority)
Esempio n. 4
0
 def parse_body(self, body, query_path_=None):
     if body is None:
         raise QueryPluginError
     body = body.strip().lower()
     # Use provided query file for testing
     if query_path_:
         query_path = query_path_
     else:
         query_path = os.path.join(get_user_dir(), 'lists', 'queries.saved')
     try:
         with open(query_path, 'rU') as query_file:
             for query_string in query_file:
                 name = next(query_file).strip().lower()
                 if name == body:
                     try:
                         return Query(query_string.strip())
                     except QueryError:
                         raise QueryPluginError
         # We've searched the whole file and haven't found a match
         raise QueryPluginError
     except IOError:
         raise QueryPluginError
     except StopIteration:
         # The file has an odd number of lines. This shouldn't happen unless
         # it has been externally modified
         raise QueryPluginError
Esempio n. 5
0
def errorhook(exc_info=None):
    """This is the main entry point

    Call in an exception context. Thread safe.

    def my_thread():
        try:
            do_work()
        except Exception:
            errorhook()
    """

    global _error_lock, _errorhook_enabled

    if not _errorhook_enabled:
        return

    if exc_info is None:
        exc_info = sys.exc_info()

    if exc_info[0] is None:
        # called outside of an exception context, just ignore
        print_e("no active exception!")
        return

    # In case something goes wrong during error handling print it first
    print_exc(exc_info)

    if not _error_lock.acquire(False):
        # Make sure only one of these is active at a time
        return

    # write error and logs to disk
    dump_dir = os.path.join(quodlibet.get_user_dir(), "dumps")
    dump_to_disk(dump_dir, exc_info)

    sentry = get_sentry()

    # For crashes the stack trace is not enough to differentiating different
    # crash sources. We need to give our own grouping key (fingerprint) based
    # on the stack trace provided by faulthandler.
    fingerprint = None
    if isinstance(exc_info[1], FaultHandlerCrash):
        fingerprint = ["{{ default }}", exc_info[1].get_grouping_key()]

    try:
        sentry_error = sentry.capture(exc_info, fingerprint=fingerprint)
    except SentryError:
        sentry_error = None

    def called_in_main_thread():
        try:
            run_error_dialogs(exc_info, sentry_error)
        finally:
            _error_lock.release()

    if is_main_thread():
        called_in_main_thread()
    else:
        GLib.idle_add(called_in_main_thread)
Esempio n. 6
0
def init():
    global browsers

    # ignore double init (for the test suite)
    if browsers:
        return

    this_dir = os.path.dirname(__file__)
    load_pyc = os.name == 'nt'
    modules = load_dir_modules(this_dir,
                               package=__package__,
                               load_compiled=load_pyc)

    user_dir = os.path.join(quodlibet.get_user_dir(), "browsers")
    if os.path.isdir(user_dir):
        modules += load_dir_modules(user_dir,
                                    package="quodlibet.fake.browsers",
                                    load_compiled=load_pyc)

    for browser in modules:
        try:
            browsers.extend(browser.browsers)
        except AttributeError:
            print_w("%r doesn't contain any browsers." % browser.__name__)

    def is_browser(Kind):
        return isinstance(Kind, type) and issubclass(Kind, Browser)
    browsers = filter(is_browser, browsers)

    if not browsers:
        raise SystemExit("No browsers found!")

    browsers.sort(key=lambda Kind: Kind.priority)
Esempio n. 7
0
    def __init__(self, parent, player, library):
        super(TopBar, self).__init__()

        # play controls
        control_item = Gtk.ToolItem()
        self.insert(control_item, 0)
        t = PlayControls(player, library.librarian)
        self.volume = t.volume

        # only restore the volume in case it is managed locally, otherwise
        # this could affect the system volume
        if not player.has_external_volume:
            player.volume = config.getfloat("memory", "volume")

        connect_destroy(player, "notify::volume", self._on_volume_changed)
        control_item.add(t)

        self.insert(Gtk.SeparatorToolItem(), 1)

        info_item = Gtk.ToolItem()
        self.insert(info_item, 2)
        info_item.set_expand(True)

        box = Gtk.Box(spacing=6)
        info_item.add(box)
        qltk.add_css(self, "GtkToolbar {padding: 3px;}")

        self._pattern_box = Gtk.VBox()

        # song text
        info_pattern_path = os.path.join(quodlibet.get_user_dir(), "songinfo")
        text = SongInfo(library.librarian, player, info_pattern_path)
        self._pattern_box.pack_start(Align(text, border=3), True, True, 0)
        box.pack_start(self._pattern_box, True, True, 0)

        # cover image
        self.image = CoverImage(resize=True)
        connect_destroy(player, 'song-started', self.__new_song)

        # FIXME: makes testing easier
        if app.cover_manager:
            connect_destroy(
                app.cover_manager, 'cover-changed',
                self.__song_art_changed, library)

        box.pack_start(Align(self.image, border=2), False, True, 0)

        # On older Gtk+ (3.4, at least)
        # setting a margin on CoverImage leads to errors and result in the
        # QL window not being visible for some reason.
        assert self.image.props.margin == 0

        for child in self.get_children():
            child.show_all()

        context = self.get_style_context()
        context.add_class("primary-toolbar")
Esempio n. 8
0
    def __init__(self,
                 filename=None,
                 completion=None,
                 accel_group=None,
                 timeout=DEFAULT_TIMEOUT,
                 validator=Query.validator,
                 star=None):
        super(SearchBarBox, self).__init__(spacing=6)

        if filename is None:
            filename = os.path.join(quodlibet.get_user_dir(), "lists",
                                    "queries")

        combo = ComboBoxEntrySave(filename,
                                  count=8,
                                  validator=validator,
                                  title=_("Saved Searches"),
                                  edit_title=_(u"Edit saved searches…"))

        self.__deferred_changed = DeferredSignal(self.__filter_changed,
                                                 timeout=timeout,
                                                 owner=self)

        self.__combo = combo
        entry = combo.get_child()
        self.__entry = entry
        if completion:
            entry.set_completion(completion)

        self._star = star
        self._query = None
        self.__sig = combo.connect('text-changed', self.__text_changed)

        entry.connect('clear', self.__filter_changed)
        entry.connect('backspace', self.__text_changed)
        entry.connect('populate-popup', self.__menu)
        entry.connect('activate', self.__filter_changed)
        entry.connect('activate', self.__save_search)
        entry.connect('focus-out-event', self.__save_search)
        entry.connect('key-press-event', self.__key_pressed)

        entry.set_placeholder_text(_("Search"))
        entry.set_tooltip_text(
            _("Search your library, "
              "using free text or QL queries"))

        combo.enable_clear_button()
        self.pack_start(combo, True, True, 0)

        if accel_group:
            key, mod = Gtk.accelerator_parse("<Primary>L")
            accel_group.connect(key, mod, 0,
                                lambda *x: entry.mnemonic_activate(True))

        for child in self.get_children():
            child.show_all()
Esempio n. 9
0
def main(argv):
    import quodlibet
    from quodlibet.qltk import add_signal_watch, icons

    add_signal_watch(app.quit)

    opts = util.OptionParser("Ex Falso", const.VERSION, _("an audio tag editor"), "[%s]" % _("directory"))

    # FIXME: support unicode on Windows, sys.argv isn't good enough
    argv.append(os.path.abspath(fsnative(u".")))
    opts, args = opts.parse(argv[1:])
    args[0] = os.path.realpath(args[0])

    config.init(os.path.join(quodlibet.get_user_dir(), "config"))

    app.name = "Ex Falso"
    app.id = "exfalso"

    quodlibet.init(icon=icons.EXFALSO, name=app.name, proc_title=app.id)

    import quodlibet.library
    import quodlibet.player

    app.library = quodlibet.library.init()
    app.player = quodlibet.player.init_player("nullbe", app.librarian)
    from quodlibet.qltk.songlist import PlaylistModel

    app.player.setup(PlaylistModel(), None, 0)
    pm = quodlibet.init_plugins()
    pm.rescan()

    from quodlibet.qltk.exfalsowindow import ExFalsoWindow

    dir_ = args[0]
    if os.name == "nt":
        dir_ = fsdecode(dir_)
    app.window = ExFalsoWindow(app.library, dir_)
    app.window.init_plugins()

    from quodlibet.util.cover import CoverManager

    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet.qltk import session

    session.init("exfalso")

    quodlibet.enable_periodic_save(save_library=False)
    quodlibet.main(app.window)

    quodlibet.finish_first_session(app.id)
    config.save()

    print_d("Finished shutdown.")
Esempio n. 10
0
def main(argv=None):
    if argv is None:
        argv = sys_argv

    import quodlibet

    config_file = os.path.join(quodlibet.get_user_dir(), "config")
    quodlibet.init(config_file=config_file)

    from quodlibet.qltk import add_signal_watch
    add_signal_watch(app.quit)

    opts = util.OptionParser(
        "Ex Falso", const.VERSION,
        _("an audio tag editor"), "[%s]" % _("directory"))

    argv.append(os.path.abspath(fsnative(u".")))
    opts, args = opts.parse(argv[1:])
    args[0] = os.path.realpath(args[0])

    app.name = "Ex Falso"
    app.description = _("Audio metadata editor")
    app.id = "exfalso"
    app.process_name = "exfalso"
    quodlibet.set_application_info(app)

    import quodlibet.library
    import quodlibet.player
    app.library = quodlibet.library.init()
    app.player = quodlibet.player.init_player("nullbe", app.librarian)
    from quodlibet.qltk.songlist import PlaylistModel
    app.player.setup(PlaylistModel(), None, 0)
    pm = quodlibet.init_plugins()
    pm.rescan()

    from quodlibet.qltk.exfalsowindow import ExFalsoWindow
    dir_ = args[0]
    app.window = ExFalsoWindow(app.library, dir_)
    app.window.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet import session
    session_client = session.init(app)

    quodlibet.enable_periodic_save(save_library=False)
    quodlibet.run(app.window)
    quodlibet.finish_first_session("exfalso")
    config.save()

    session_client.close()

    util.print_d("Finished shutdown.")
Esempio n. 11
0
def main(argv=None):
    if argv is None:
        argv = sys_argv

    import quodlibet

    config_file = os.path.join(quodlibet.get_user_dir(), "config")
    quodlibet.init(config_file=config_file)

    from quodlibet.qltk import add_signal_watch
    add_signal_watch(app.quit)

    opts = util.OptionParser("Ex Falso", const.VERSION,
                             _("an audio tag editor"), "[%s]" % _("directory"))

    argv.append(os.path.abspath(fsnative(u".")))
    opts, args = opts.parse(argv[1:])
    args[0] = os.path.realpath(args[0])

    app.name = "Ex Falso"
    app.description = _("Audio metadata editor")
    app.id = "exfalso"
    app.process_name = "exfalso"
    quodlibet.set_application_info(app)

    import quodlibet.library
    import quodlibet.player
    app.library = quodlibet.library.init()
    app.player = quodlibet.player.init_player("nullbe", app.librarian)
    from quodlibet.qltk.songlist import PlaylistModel
    app.player.setup(PlaylistModel(), None, 0)
    pm = quodlibet.init_plugins()
    pm.rescan()

    from quodlibet.qltk.exfalsowindow import ExFalsoWindow
    dir_ = args[0]
    app.window = ExFalsoWindow(app.library, dir_)
    app.window.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet import session
    session_client = session.init(app)

    quodlibet.enable_periodic_save(save_library=False)
    quodlibet.run(app.window)
    quodlibet.finish_first_session("exfalso")
    config.save()

    session_client.close()

    util.print_d("Finished shutdown.")
Esempio n. 12
0
def main(argv):
    global quodlibet

    from quodlibet.qltk import add_signal_watch, icons
    add_signal_watch(app.quit)

    opts = util.OptionParser("Ex Falso", const.VERSION,
                             _("an audio tag editor"), "[%s]" % _("directory"))

    # FIXME: support unicode on Windows, sys.argv isn't good enough
    argv.append(os.path.abspath(fsnative(u".")))
    opts, args = opts.parse(argv[1:])
    args[0] = os.path.realpath(args[0])

    config.init(os.path.join(quodlibet.get_user_dir(), "config"))

    app.name = "Ex Falso"
    app.id = "exfalso"

    quodlibet.init(icon=icons.EXFALSO, name=app.name, proc_title=app.id)

    import quodlibet.library
    import quodlibet.player
    app.library = quodlibet.library.init()
    app.player = quodlibet.player.init_player("nullbe", app.librarian)
    from quodlibet.qltk.songlist import PlaylistModel
    app.player.setup(PlaylistModel(), None, 0)
    pm = quodlibet.init_plugins()
    pm.rescan()

    from quodlibet.qltk.exfalsowindow import ExFalsoWindow
    dir_ = args[0]
    if os.name == "nt":
        dir_ = fsdecode(dir_)
    app.window = ExFalsoWindow(app.library, dir_)
    app.window.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet.qltk import session
    session.init("exfalso")

    quodlibet.enable_periodic_save(save_library=False)
    quodlibet.main(app.window)

    quodlibet.finish_first_session(app.id)
    config.save()

    print_d("Finished shutdown.")
Esempio n. 13
0
def update_feeds(subscriptions):
    feeds = []
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "rb") as f:
        try:
            feeds = pickle.load(f)
        except:
            print_d("Couldn't read feeds.")

    subbed = frozenset([f.uri for f in feeds])
    newfeeds = list()

    for subscription in subscriptions:
        try:
            r = requests.get(subscription)
        except requests.exceptions.ConnectionError as e:
            print_d("ConnectionError %s - %s" % (subscription, e));
            continue

        if not r.status_code == 200:
            print_d("Cannot access %s - %i" % (subscription, r.status_code))
            continue

        feed = Feed(subscription)
        if feed.uri in subbed:
            print_d("Feed already subscribed: %s" % subscription)
            continue
        feed.changed = feed.parse()
        if feed:
            print_d("Appending %s" % subscription)
            feeds.append(feed)
            newfeeds.append(feed)
        else:
            print_d("Feed could not be added: %s" % subscription)

    print_d("Adding %i feeds." % len(newfeeds))
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "wb") as f:
        pickle.dump(feeds, f)
    app.browser.reload(app.library)  # adds feeds
Esempio n. 14
0
def update_feeds(subscriptions):
    feeds = []
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "rb") as f:
        try:
            feeds = pickle.load(f)
        except:
            print_d("Couldn't read feeds.")

    subbed = frozenset([f.uri for f in feeds])
    newfeeds = list()

    for subscription in subscriptions:
        try:
            r = requests.get(subscription)
        except requests.exceptions.ConnectionError as e:
            print_d("ConnectionError %s - %s" % (subscription, e))
            continue

        if not r.status_code == 200:
            print_d("Cannot access %s - %i" % (subscription, r.status_code))
            continue

        feed = Feed(subscription)
        if feed.uri in subbed:
            print_d("Feed already subscribed: %s" % subscription)
            continue
        feed.changed = feed.parse()
        if feed:
            print_d("Appending %s" % subscription)
            feeds.append(feed)
            newfeeds.append(feed)
        else:
            print_d("Feed could not be added: %s" % subscription)

    print_d("Adding %i feeds." % len(newfeeds))
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "wb") as f:
        pickle.dump(feeds, f)
    app.browser.reload(app.library)  # adds feeds
Esempio n. 15
0
    def __init__(self, filename=None, completion=None, accel_group=None,
                 timeout=DEFAULT_TIMEOUT, validator=Query.validator,
                 star=None):
        super(SearchBarBox, self).__init__(spacing=6)

        if filename is None:
            filename = os.path.join(
                quodlibet.get_user_dir(), "lists", "queries")

        combo = ComboBoxEntrySave(filename, count=8,
                                  validator=validator,
                                  title=_("Saved Searches"),
                                  edit_title=_(u"Edit saved searches…"))

        self.__deferred_changed = DeferredSignal(
            self.__filter_changed, timeout=timeout, owner=self)

        self.__combo = combo
        entry = combo.get_child()
        self.__entry = entry
        if completion:
            entry.set_completion(completion)

        self._star = star
        self._query = None
        self.__sig = combo.connect('text-changed', self.__text_changed)

        entry.connect('clear', self.__filter_changed)
        entry.connect('backspace', self.__text_changed)
        entry.connect('populate-popup', self.__menu)
        entry.connect('activate', self.__filter_changed)
        entry.connect('activate', self.__save_search)
        entry.connect('focus-out-event', self.__save_search)
        entry.connect('key-press-event', self.__key_pressed)

        entry.set_placeholder_text(_("Search"))
        entry.set_tooltip_text(_("Search your library, "
                                 "using free text or QL queries"))

        combo.enable_clear_button()
        self.pack_start(combo, True, True, 0)

        if accel_group:
            key, mod = Gtk.accelerator_parse("<Primary>L")
            accel_group.connect(key, mod, 0,
                    lambda *x: entry.mnemonic_activate(True))

        for child in self.get_children():
            child.show_all()
Esempio n. 16
0
def get_thumbnail_folder():
    """Returns a path to the thumbnail folder.

    The returned path might not exist.
    """

    if os.name == "nt":
        thumb_folder = os.path.join(quodlibet.get_user_dir(), "thumbnails")
    else:
        cache_folder = os.path.join(xdg_get_cache_home(), "thumbnails")
        thumb_folder = os.path.expanduser('~/.thumbnails')
        if os.path.exists(cache_folder) or not os.path.exists(thumb_folder):
            thumb_folder = cache_folder

    return thumb_folder
Esempio n. 17
0
class QuodLibetUnixRemote(RemoteBase):

    _FIFO_NAME = "control"
    _PATH = os.path.join(get_user_dir(), _FIFO_NAME)

    def __init__(self, app, cmd_registry):
        self._app = app
        self._cmd_registry = cmd_registry
        self._fifo = fifo.FIFO(self._PATH, self._callback)

    @classmethod
    def remote_exists(cls):
        return fifo.fifo_exists(cls._PATH)

    @classmethod
    def send_message(cls, message):
        assert isinstance(message, fsnative)

        try:
            return fifo.write_fifo(cls._PATH, fsn2bytes(message, None))
        except EnvironmentError as e:
            raise RemoteError(e)

    def start(self):
        try:
            self._fifo.open()
        except fifo.FIFOError as e:
            raise RemoteError(e)

    def stop(self):
        self._fifo.destroy()

    def _callback(self, data):
        try:
            messages = list(fifo.split_message(data))
        except ValueError:
            print_w("invalid message: %r" % data)
            return

        for command, path in messages:
            command = bytes2fsn(command, None)
            response = self._cmd_registry.handle_line(self._app, command)
            if path is not None:
                path = bytes2fsn(path, None)
                with open(path, "wb") as h:
                    if response is not None:
                        assert isinstance(response, fsnative)
                        h.write(fsn2bytes(response, None))
Esempio n. 18
0
def init():
    """Import all browsers from this package and from the user directory.

    After this is called the global `browers` list will contain all
    classes sorted by priority.

    Can be called multiple times.
    """

    global browsers, default

    # ignore double init (for the test suite)
    if browsers:
        return

    this_dir = os.path.dirname(__file__)
    load_pyc = util.is_windows() or util.is_osx()
    modules = load_dir_modules(this_dir,
                               package=__package__,
                               load_compiled=load_pyc)

    user_dir = os.path.join(quodlibet.get_user_dir(), "browsers")
    if os.path.isdir(user_dir):
        modules += load_dir_modules(user_dir,
                                    package="quodlibet.fake.browsers",
                                    load_compiled=load_pyc)

    for browser in modules:
        try:
            browsers.extend(browser.browsers)
        except AttributeError:
            print_w("%r doesn't contain any browsers." % browser.__name__)

    def is_browser(Kind):
        return isinstance(Kind, type) and issubclass(Kind, Browser)

    browsers = list(filter(is_browser, browsers))

    if not browsers:
        raise SystemExit("No browsers found!")

    browsers.sort(key=lambda Kind: Kind.priority)

    try:
        default = get("SearchBar")
    except ValueError:
        raise SystemExit("Default browser not found!")
Esempio n. 19
0
def init():
    """Import all browsers from this package and from the user directory.

    After this is called the global `browers` list will contain all
    classes sorted by priority.

    Can be called multiple times.
    """

    global browsers, default

    # ignore double init (for the test suite)
    if browsers:
        return

    this_dir = os.path.dirname(__file__)
    load_pyc = util.is_windows() or util.is_osx()
    modules = load_dir_modules(this_dir,
                               package=__package__,
                               load_compiled=load_pyc)

    user_dir = os.path.join(quodlibet.get_user_dir(), "browsers")
    if os.path.isdir(user_dir):
        modules += load_dir_modules(user_dir,
                                    package="quodlibet.fake.browsers",
                                    load_compiled=load_pyc)

    for browser in modules:
        try:
            browsers.extend(browser.browsers)
        except AttributeError:
            print_w("%r doesn't contain any browsers." % browser.__name__)

    def is_browser(Kind):
        return isinstance(Kind, type) and issubclass(Kind, Browser)
    browsers = filter(is_browser, browsers)

    if not browsers:
        raise SystemExit("No browsers found!")

    browsers.sort(key=lambda Kind: Kind.priority)

    try:
        default = get("SearchBar")
    except ValueError:
        raise SystemExit("Default browser not found!")
Esempio n. 20
0
    def __init__(self, *args, show_multi=False, multi_filename=None, **kwargs):
        super().__init__(*args, **kwargs)

        self.multi_filename = os.path.join(
            quodlibet.get_user_dir(), "lists",
            "multiqueries") if multi_filename is None else multi_filename

        self._old_placeholder = self._entry.get_placeholder_text()
        self._old_tooltip = self._entry.get_tooltip_text()

        self._add_button = Gtk.Button.new_from_icon_name(
            "list-add", Gtk.IconSize.BUTTON)
        self._add_button.set_no_show_all(True)
        self.pack_start(self._add_button, False, True, 0)
        self._add_button.connect('clicked', self.activated)
        self._entry.connect('activate', self.activated)

        self.flow_box = Gtk.FlowBox(no_show_all=True,
                                    max_children_per_line=99,
                                    selection_mode=Gtk.SelectionMode.NONE)

        self.toggle_multi_bool(show_multi)
Esempio n. 21
0
def update_feeds(subscriptions):
    if app.browser.name != u"Audio Feeds":
        print_d("Wrong browser!")
        return

    feeds = []
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "rb") as f:
        try:
            feeds = pickle.load(f)
        except:
            print_d("Couldn't read feeds.")

    subbed = frozenset([f.uri for f in feeds])
    newfeeds = list()

    for subscription in subscriptions:
        try:
            r = requests.get(subscription)
        except requests.exceptions.ConnectionError, e:
            print_d("ConnectionError %s - %s" % (subscription, e));
            continue

        if not r.status_code == 200:
            print_d("Cannot access %s - %i" % (subscription, r.status_code))
            continue

        feed = Feed(subscription)
        if feed.uri in subbed:
            print_d("Feed already subscribed: %s" % subscription)
            continue
        feed.changed = feed.parse()
        if feed:
            print_d("Appending %s" % subscription)
            feeds.append(feed)
            newfeeds.append(feed)
        else:
            print_d("Feed could not be added: %s" % subscription)
Esempio n. 22
0
def main(argv=None):
    if argv is None:
        argv = sys_argv

    import quodlibet

    config_file = os.path.join(quodlibet.get_user_dir(), "config")
    quodlibet.init_cli(config_file=config_file)

    try:
        # we want basic commands not to import gtk (doubles process time)
        assert "gi.repository.Gtk" not in sys.modules
        sys.modules["gi.repository.Gtk"] = None
        startup_actions, cmds_todo = process_arguments(argv)
    finally:
        sys.modules.pop("gi.repository.Gtk", None)

    quodlibet.init()

    from quodlibet import app
    from quodlibet.qltk import add_signal_watch, Icons
    add_signal_watch(app.quit)

    import quodlibet.player
    import quodlibet.library
    from quodlibet import config
    from quodlibet import browsers
    from quodlibet import util

    app.name = "Quod Libet"
    app.description = _("Music player and music library manager")
    app.id = "quodlibet"
    quodlibet.set_application_info(Icons.QUODLIBET, app.id, app.name)

    library_path = os.path.join(quodlibet.get_user_dir(), "songs")

    print_d("Initializing main library (%s)" % (
            quodlibet.util.path.unexpand(library_path)))

    library = quodlibet.library.init(library_path)
    app.library = library

    # this assumes that nullbe will always succeed
    from quodlibet.player import PlayerError
    wanted_backend = environ.get(
        "QUODLIBET_BACKEND", config.get("player", "backend"))

    try:
        player = quodlibet.player.init_player(wanted_backend, app.librarian)
    except PlayerError:
        print_exc()
        player = quodlibet.player.init_player("nullbe", app.librarian)

    app.player = player

    environ["PULSE_PROP_media.role"] = "music"
    environ["PULSE_PROP_application.icon_name"] = "quodlibet"

    browsers.init()

    from quodlibet.qltk.songlist import SongList, get_columns

    headers = get_columns()
    SongList.set_all_column_headers(headers)

    for opt in config.options("header_maps"):
        val = config.get("header_maps", opt)
        util.tags.add(opt, val)

    in_all = ("~filename ~uri ~#lastplayed ~#rating ~#playcount ~#skipcount "
              "~#added ~#bitrate ~current ~#laststarted ~basename "
              "~dirname").split()
    for Kind in browsers.browsers:
        if Kind.headers is not None:
            Kind.headers.extend(in_all)
        Kind.init(library)

    pm = quodlibet.init_plugins("no-plugins" in startup_actions)

    if hasattr(player, "init_plugins"):
        player.init_plugins()

    from quodlibet.qltk import unity
    unity.init("quodlibet.desktop", player)

    from quodlibet.qltk.songsmenu import SongsMenu
    SongsMenu.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet.plugins.playlist import PLAYLIST_HANDLER
    PLAYLIST_HANDLER.init_plugins()

    from quodlibet.plugins.query import QUERY_HANDLER
    QUERY_HANDLER.init_plugins()

    from gi.repository import GLib

    def exec_commands(*args):
        for cmd in cmds_todo:
            try:
                resp = cmd_registry.run(app, *cmd)
            except CommandError:
                pass
            else:
                if resp is not None:
                    print_(resp, end="", flush=True)

    from quodlibet.qltk.quodlibetwindow import QuodLibetWindow, PlayerOptions
    # Call exec_commands after the window is restored, but make sure
    # it's after the mainloop has started so everything is set up.

    app.window = window = QuodLibetWindow(
        library, player,
        restore_cb=lambda:
            GLib.idle_add(exec_commands, priority=GLib.PRIORITY_HIGH))

    app.player_options = PlayerOptions(window)

    from quodlibet.qltk.window import Window

    from quodlibet.plugins.events import EventPluginHandler
    from quodlibet.plugins.gui import UserInterfacePluginHandler
    pm.register_handler(EventPluginHandler(library.librarian, player,
                                           app.window.songlist))
    pm.register_handler(UserInterfacePluginHandler())

    from quodlibet.mmkeys import MMKeysHandler
    from quodlibet.remote import Remote, RemoteError
    from quodlibet.commands import registry as cmd_registry, CommandError
    from quodlibet.qltk.tracker import SongTracker, FSInterface
    try:
        from quodlibet.qltk.dbus_ import DBusHandler
    except ImportError:
        DBusHandler = lambda player, library: None

    mmkeys_handler = MMKeysHandler(app)
    mmkeys_handler.start()

    current_path = os.path.join(quodlibet.get_user_dir(), "current")
    fsiface = FSInterface(current_path, player)
    remote = Remote(app, cmd_registry)
    try:
        remote.start()
    except RemoteError:
        exit_(1, True)

    DBusHandler(player, library)
    tracker = SongTracker(library.librarian, player, window.playlist)

    from quodlibet.qltk import session
    session.init("quodlibet")

    quodlibet.enable_periodic_save(save_library=True)

    if "start-playing" in startup_actions:
        player.paused = False

    if "start-hidden" in startup_actions:
        Window.prevent_inital_show(True)

    # restore browser windows
    from quodlibet.qltk.browser import LibraryBrowser
    GLib.idle_add(LibraryBrowser.restore, library, player,
                  priority=GLib.PRIORITY_HIGH)

    def before_quit():
        print_d("Saving active browser state")
        try:
            app.browser.save()
        except NotImplementedError:
            pass

        print_d("Shutting down player device %r." % player.version_info)
        player.destroy()

    quodlibet.run(window, before_quit=before_quit)

    app.player_options.destroy()
    quodlibet.finish_first_session(app.id)
    mmkeys_handler.quit()
    remote.stop()
    fsiface.destroy()

    tracker.destroy()
    quodlibet.library.save()

    config.save()

    print_d("Finished shutdown.")
Esempio n. 23
0
    connect_obj, escape
from quodlibet.util.i18n import numeric_phrase
from quodlibet.util.path import uri_is_valid
from quodlibet.util.string import decode, encode
from quodlibet.util import print_w
from quodlibet.qltk.views import AllTreeView
from quodlibet.qltk.searchbar import SearchBarBox
from quodlibet.qltk.completion import LibraryTagCompletion
from quodlibet.qltk.x import MenuItem, Align, ScrolledWindow
from quodlibet.qltk.x import SymbolicIconImage
from quodlibet.qltk.menubutton import MenuButton


STATION_LIST_URL = \
    "https://quodlibet.github.io/radio/radiolist.bz2"
STATIONS_FAV = os.path.join(quodlibet.get_user_dir(), "stations")
STATIONS_ALL = os.path.join(quodlibet.get_user_dir(), "stations_all")

# TODO: - Do the update in a thread
#       - Ranking: reduce duplicate stations (max 3 URLs per station)
#                  prefer stations that match a genre?

# Migration path for pickle
sys.modules["browsers.iradio"] = sys.modules[__name__]


class IRFile(RemoteFile):
    multisong = True
    can_add = False

    format = "Radio Station"
Esempio n. 24
0
class LastFMSync(SongsMenuPlugin):
    PLUGIN_ID = "Last.fm Sync"
    PLUGIN_NAME = _("Last.fm Sync")
    PLUGIN_DESC = _("Updates your library's statistics from your "
                    "Last.fm profile.")
    PLUGIN_ICON = Icons.EMBLEM_SHARED

    CACHE_PATH = os.path.join(quodlibet.get_user_dir(), "lastfmsync.db")

    def runner(self, cache):
        changed = True
        try:
            changed = cache.update_charts(self.progress)
        except:
            pass
        if changed:
            self.cache_shelf[cache.username] = cache
        self.cache_shelf.close()

    def progress(self, msg, frac):
        if self.running:
            GLib.idle_add(self.dialog.progress, msg, frac)
            return True
        else:
            return False

    def plugin_songs(self, songs):
        try:
            self.cache_shelf = shelve.open(self.CACHE_PATH)
        except:
            # some Python 2 DB types can't be opened in Python 3
            self.cache_shelf = shelve.open(self.CACHE_PATH, "n")

        user = config_get('username', '')
        try:
            cache = self.cache_shelf.setdefault(user, LastFMSyncCache(user))
        except Exception:
            # unpickle can fail in many ways. this is just cache, so ignore
            cache = self.cache_shelf[user] = LastFMSyncCache(user)

        self.dialog = LastFMSyncWindow(self.plugin_window)
        self.running = True
        thread = Thread(target=self.runner, args=(cache,))
        thread.daemon = True
        thread.start()
        resp = self.dialog.run()
        if resp == Gtk.ResponseType.ACCEPT:
            cache.update_songs(songs)
        self.running = False
        self.dialog.destroy()

    @classmethod
    def PluginPreferences(klass, win):
        def entry_changed(entry):
            config.set('plugins', 'lastfmsync_username', entry.get_text())

        label = Gtk.Label(label=_("_Username:"******"Account"), child=hbox)
Esempio n. 25
0
from quodlibet.qltk.getstring import GetStringDialog
from quodlibet.qltk.songsmenu import SongsMenu
from quodlibet.qltk.notif import Task
from quodlibet.qltk import Icons
from quodlibet.util import copool, connect_destroy, sanitize_tags, connect_obj
from quodlibet.util.string import decode, encode
from quodlibet.util.uri import URI
from quodlibet.qltk.views import AllTreeView
from quodlibet.qltk.searchbar import SearchBarBox
from quodlibet.qltk.completion import LibraryTagCompletion
from quodlibet.qltk.x import MenuItem, Align, ScrolledWindow
from quodlibet.qltk.x import SymbolicIconImage
from quodlibet.qltk.menubutton import MenuButton

STATION_LIST_URL = "http://bitbucket.org/lazka/quodlibet/downloads/radiolist.bz2"
STATIONS_FAV = os.path.join(quodlibet.get_user_dir(), "stations")
STATIONS_ALL = os.path.join(quodlibet.get_user_dir(), "stations_all")

# TODO: - Do the update in a thread
#       - Ranking: reduce duplicate stations (max 3 URLs per station)
#                  prefer stations that match a genre?

# Migration path for pickle
sys.modules["browsers.iradio"] = sys.modules[__name__]


class IRFile(RemoteFile):
    multisong = True
    can_add = False

    format = "Radio Station"
Esempio n. 26
0
# (at your option) any later version.

import os
import re
from typing import Iterable, Generator, Optional

import quodlibet
from quodlibet import print_d, print_w
from quodlibet.formats import AudioFile
from quodlibet.library.base import Library
from quodlibet.util.collection import (Playlist, XSPFBackedPlaylist,
                                       FileBackedPlaylist)
from senf import text2fsn, _fsnative, fsn2text

_DEFAULT_PLAYLIST_DIR = text2fsn(
    os.path.join(quodlibet.get_user_dir(), "playlists"))
"""Directory for playlist files"""

HIDDEN_RE = re.compile(r'^\.\w[^.]*')
"""Hidden-like files, to ignored"""


class PlaylistLibrary(Library[str, Playlist]):
    """A PlaylistLibrary listens to a SongLibrary, and keeps tracks of playlists
    of these songs.

    The library behaves like a dictionary: the keys are playlist names,
    the values are Playlist objects.
    """
    def __init__(self,
                 library: Library,
Esempio n. 27
0
from glob import glob

from gi.repository import Gtk, GLib, GdkPixbuf

import quodlibet
from quodlibet import util
from quodlibet import app

from quodlibet.devices._base import Device
from quodlibet.library import SongFileLibrary
from quodlibet.pattern import FileFromPattern
from quodlibet.qltk.msg import ConfirmFileReplace
from quodlibet.util.path import (mtime, escape_filename,
    strip_win32_incompat_from_path)

CACHE = os.path.join(quodlibet.get_user_dir(), 'cache')


class StorageDevice(Device):
    protocol = 'storage'

    defaults = {
        'pattern': '<artist>/<album>/<title>',
        'covers': True,
        'unclutter': True,
    }

    __library = None
    __pattern = None

    def __init__(self, backend_id, device_id):
Esempio n. 28
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super(QuodLibetWindow, self).__init__(dialog=False)
        self.last_dir = get_home_dir()

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(550, 450)

        main_box = Gtk.VBox()
        self.add(main_box)

        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, True)

        keyval, mod = Gtk.accelerator_parse("<control><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # dbus app menu
        # Unity puts the app menu next to our menu bar. Since it only contains
        # menu items also available in the menu bar itself, don't add it.
        if not util.is_unity():
            AppMenu(self, ui.get_action_groups()[0])

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.show_all()
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after('drag-data-received',
                                    self.__songlist_drag_data_recv)
        self.song_scroller = SongListScroller(
            ui.get_widget("/Menu/View/SongList"))
        self.song_scroller.add(self.songlist)
        self.qexpander = QueueExpander(ui.get_widget("/Menu/View/Queue"),
                                       library, player)
        self.playlist = PlaylistMux(player, self.qexpander.model,
                                    self.songlist.model)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        main_box.pack_start(self.__browserbox, True, True, 0)

        statusbox = StatusBarBox(self.songlist.model, player)
        self.order = statusbox.order
        self.repeat = statusbox.repeat
        self.statusbar = statusbox.statusbar

        main_box.pack_start(Align(statusbox, border=3, top=-3, right=3), False,
                            True, 0)

        self.songpane = ConfigRVPaned("memory", "queue_position", 0.75)
        self.songpane.pack1(self.song_scroller, resize=True, shrink=False)
        self.songpane.pack2(self.qexpander, resize=True, shrink=False)
        self.__handle_position = self.songpane.get_property("position")

        def songpane_button_press_cb(pane, event):
            """If we start to drag the pane handle while the
            queue expander is unexpanded, expand it and move the handle
            to the bottom, so we can 'drag' the queue out
            """

            if event.window != pane.get_handle_window():
                return False

            if not self.qexpander.get_expanded():
                self.qexpander.set_expanded(True)
                pane.set_relative(1.0)
            return False

        self.songpane.connect("button-press-event", songpane_button_press_cb)

        self.song_scroller.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::expanded', self.__expand_or)
        self.qexpander.connect('draw', self.__qex_size_allocate)
        self.songpane.connect('notify', self.__moved_pane_handle)

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self.select_browser(self, config.get("memory", "browser"), library,
                                player, restore_browser)
        except:
            config.set("memory", "browser", browsers.name(0))
            config.save()
            raise

        self.showhide_playlist(ui.get_widget("/Menu/View/SongList"))
        self.showhide_playqueue(ui.get_widget("/Menu/View/Queue"))

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_time)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Esempio n. 29
0
def main(argv):
    import quodlibet

    quodlibet.init_cli()

    try:
        # we want basic commands not to import gtk (doubles process time)
        assert "gi.repository.Gtk" not in sys.modules
        sys.modules["gi.repository.Gtk"] = None
        startup_actions, cmds_todo = process_arguments(argv)
    finally:
        sys.modules.pop("gi.repository.Gtk", None)

    quodlibet.init()

    from quodlibet import app
    from quodlibet.qltk import add_signal_watch, Icons
    add_signal_watch(app.quit)

    import quodlibet.player
    import quodlibet.library
    from quodlibet import config
    from quodlibet import browsers
    from quodlibet import util
    from quodlibet.util.string import decode

    app.name = "Quod Libet"
    app.id = "quodlibet"
    quodlibet.set_application_info(Icons.QUODLIBET, app.id, app.name)

    config.init(os.path.join(quodlibet.get_user_dir(), "config"))

    library_path = os.path.join(quodlibet.get_user_dir(), "songs")

    print_d("Initializing main library (%s)" % (
            quodlibet.util.path.unexpand(library_path)))

    library = quodlibet.library.init(library_path)
    app.library = library

    # this assumes that nullbe will always succeed
    from quodlibet.player import PlayerError
    wanted_backend = os.environ.get(
        "QUODLIBET_BACKEND", config.get("player", "backend"))
    backend_traceback = None
    for backend in [wanted_backend, "nullbe"]:
        try:
            player = quodlibet.player.init_player(backend, app.librarian)
        except PlayerError:
            backend_traceback = decode(traceback.format_exc())
        else:
            break
    app.player = player

    os.environ["PULSE_PROP_media.role"] = "music"
    os.environ["PULSE_PROP_application.icon_name"] = "quodlibet"

    browsers.init()

    from quodlibet.qltk.songlist import SongList, get_columns

    from quodlibet.util.collection import Album
    try:
        cover_size = config.getint("browsers", "cover_size")
    except config.Error:
        pass
    else:
        if cover_size > 0:
            Album.COVER_SIZE = cover_size

    headers = get_columns()
    SongList.set_all_column_headers(headers)

    for opt in config.options("header_maps"):
        val = config.get("header_maps", opt)
        util.tags.add(opt, val)

    in_all = ("~filename ~uri ~#lastplayed ~#rating ~#playcount ~#skipcount "
              "~#added ~#bitrate ~current ~#laststarted ~basename "
              "~dirname").split()
    for Kind in browsers.browsers:
        if Kind.headers is not None:
            Kind.headers.extend(in_all)
        Kind.init(library)

    pm = quodlibet.init_plugins("no-plugins" in startup_actions)

    if hasattr(player, "init_plugins"):
        player.init_plugins()

    from quodlibet.qltk import unity
    unity.init("quodlibet.desktop", player)

    from quodlibet.qltk.songsmenu import SongsMenu
    SongsMenu.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet.plugins.playlist import PLAYLIST_HANDLER
    PLAYLIST_HANDLER.init_plugins()

    from gi.repository import GLib

    def exec_commands(*args):
        for cmd in cmds_todo:
            try:
                resp = cmd_registry.run(app, *cmd)
            except CommandError:
                pass
            else:
                if resp is not None:
                    print_(resp, end="")

    from quodlibet.qltk.quodlibetwindow import QuodLibetWindow, PlayerOptions
    # Call exec_commands after the window is restored, but make sure
    # it's after the mainloop has started so everything is set up.
    app.window = window = QuodLibetWindow(
        library, player,
        restore_cb=lambda:
            GLib.idle_add(exec_commands, priority=GLib.PRIORITY_HIGH))

    app.player_options = PlayerOptions(window)

    from quodlibet.qltk.debugwindow import MinExceptionDialog
    from quodlibet.qltk.window import on_first_map
    if backend_traceback is not None:
        def show_backend_error(window):
            d = MinExceptionDialog(window,
                _("Audio Backend Failed to Load"),
                _("Loading the audio backend '%(name)s' failed. "
                  "Audio playback will be disabled.") %
                {"name": wanted_backend},
                backend_traceback)
            d.run()

        # so we show the main window first
        on_first_map(app.window, show_backend_error, app.window)

    from quodlibet.plugins.events import EventPluginHandler
    pm.register_handler(EventPluginHandler(library.librarian, player))

    from quodlibet.mmkeys import MMKeysHandler
    from quodlibet.remote import Remote, RemoteError
    from quodlibet.commands import registry as cmd_registry, CommandError
    from quodlibet.qltk.tracker import SongTracker, FSInterface
    try:
        from quodlibet.qltk.dbus_ import DBusHandler
    except ImportError:
        DBusHandler = lambda player, library: None

    mmkeys_handler = MMKeysHandler(app.name, window, player)
    if "QUODLIBET_NO_MMKEYS" not in os.environ:
        mmkeys_handler.start()
    current_path = os.path.join(quodlibet.get_user_dir(), "current")
    fsiface = FSInterface(current_path, player)
    remote = Remote(app, cmd_registry)
    try:
        remote.start()
    except RemoteError:
        exit_(1, True)

    DBusHandler(player, library)
    tracker = SongTracker(library.librarian, player, window.playlist)

    from quodlibet.qltk import session
    session.init("quodlibet")

    quodlibet.enable_periodic_save(save_library=True)

    if "start-playing" in startup_actions:
        player.paused = False

    # restore browser windows
    from quodlibet.qltk.browser import LibraryBrowser
    GLib.idle_add(LibraryBrowser.restore, library, player,
                  priority=GLib.PRIORITY_HIGH)

    def before_quit():
        print_d("Saving active browser state")
        try:
            app.browser.save()
        except NotImplementedError:
            pass

        print_d("Shutting down player device %r." % player.version_info)
        player.destroy()

    quodlibet.main(window, before_quit=before_quit)

    app.player_options.destroy()
    quodlibet.finish_first_session(app.id)
    mmkeys_handler.quit()
    remote.stop()
    fsiface.destroy()

    tracker.destroy()
    quodlibet.library.save()

    config.save()

    print_d("Finished shutdown.")
Esempio n. 30
0
 def test_dirs(self):
     self.assertTrue(isinstance(quodlibet.get_base_dir(), fsnative))
     self.assertTrue(isinstance(quodlibet.get_image_dir(), fsnative))
     self.assertTrue(isinstance(quodlibet.get_user_dir(), fsnative))
Esempio n. 31
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super(QuodLibetWindow, self).__init__(dialog=False)
        self.last_dir = get_home_dir()

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(550, 450)

        main_box = Gtk.VBox()
        self.add(main_box)

        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, True)

        keyval, mod = Gtk.accelerator_parse("<control><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # dbus app menu
        # Unity puts the app menu next to our menu bar. Since it only contains
        # menu items also available in the menu bar itself, don't add it.
        if not util.is_unity():
            AppMenu(self, ui.get_action_groups()[0])

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.show_all()
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after(
            'drag-data-received', self.__songlist_drag_data_recv)
        self.song_scroller = SongListScroller(
            ui.get_widget("/Menu/View/SongList"))
        self.song_scroller.add(self.songlist)
        self.qexpander = QueueExpander(
            ui.get_widget("/Menu/View/Queue"), library, player)
        self.playlist = PlaylistMux(
            player, self.qexpander.model, self.songlist.model)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        main_box.pack_start(self.__browserbox, True, True, 0)

        statusbox = StatusBarBox(self.songlist.model, player)
        self.order = statusbox.order
        self.repeat = statusbox.repeat
        self.statusbar = statusbox.statusbar

        main_box.pack_start(
            Align(statusbox, border=3, top=-3, right=3),
            False, True, 0)

        self.songpane = ConfigRVPaned("memory", "queue_position", 0.75)
        self.songpane.pack1(self.song_scroller, resize=True, shrink=False)
        self.songpane.pack2(self.qexpander, resize=True, shrink=False)
        self.__handle_position = self.songpane.get_property("position")

        def songpane_button_press_cb(pane, event):
            """If we start to drag the pane handle while the
            queue expander is unexpanded, expand it and move the handle
            to the bottom, so we can 'drag' the queue out
            """

            if event.window != pane.get_handle_window():
                return False

            if not self.qexpander.get_expanded():
                self.qexpander.set_expanded(True)
                pane.set_relative(1.0)
            return False

        self.songpane.connect("button-press-event", songpane_button_press_cb)

        self.song_scroller.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::expanded', self.__expand_or)
        self.qexpander.connect('draw', self.__qex_size_allocate)
        self.songpane.connect('notify', self.__moved_pane_handle)

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self.select_browser(
                self, config.get("memory", "browser"), library, player,
                restore_browser)
        except:
            config.set("memory", "browser", browsers.name(0))
            config.save()
            raise

        self.showhide_playlist(ui.get_widget("/Menu/View/SongList"))
        self.showhide_playqueue(ui.get_widget("/Menu/View/Queue"))

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_time)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(
            Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Esempio n. 32
0
import quodlibet
from quodlibet import config
from quodlibet import util
from quodlibet import qltk

from quodlibet.util import connect_obj, connect_destroy, format_time_preferred
from quodlibet.qltk import Icons, gtk_version, add_css
from quodlibet.qltk.ccb import ConfigCheckButton
from quodlibet.qltk.songlist import SongList, DND_QL, DND_URI_LIST
from quodlibet.qltk.songsmenu import SongsMenu
from quodlibet.qltk.songmodel import PlaylistModel
from quodlibet.qltk.playorder import OrderInOrder, OrderShuffle
from quodlibet.qltk.x import ScrolledWindow, SymbolicIconImage, \
    SmallImageButton, MenuItem

QUEUE = os.path.join(quodlibet.get_user_dir(), "queue")


class PlaybackStatusIcon(Gtk.Box):
    """A widget showing a play/pause/stop symbolic icon"""

    def __init__(self):
        super(PlaybackStatusIcon, self).__init__()
        self._icons = {}

    def _set(self, name):
        if name not in self._icons:
            image = SymbolicIconImage(name, Gtk.IconSize.MENU)
            self._icons[name] = image
            image.show()
        else:
Esempio n. 33
0
            continue

        feed = Feed(subscription)
        if feed.uri in subbed:
            print_d("Feed already subscribed: %s" % subscription)
            continue
        feed.changed = feed.parse()
        if feed:
            print_d("Appending %s" % subscription)
            feeds.append(feed)
            newfeeds.append(feed)
        else:
            print_d("Feed could not be added: %s" % subscription)

    print_d("Adding %i feeds." % len(newfeeds))
    with open(os.path.join(quodlibet.get_user_dir(), "feeds"), "wb") as f:
        pickle.dump(feeds, f)
    app.browser.reload(app.library)  # adds feeds

    #app.browser.restore()


def fetch_opml(url):
    try:
        outline = opml.parse(url)
    except IOError:
        print_d("Failed opening OPML %s" % url)
        return []
    GObject.idle_add(lambda: update_feeds([x.xmlUrl for x in outline]))

Esempio n. 34
0
from quodlibet import app

from quodlibet.util import (connect_destroy, connect_after_destroy,
                            format_time_preferred, print_exc, DeferredSignal)
from quodlibet.qltk import Icons, gtk_version, add_css
from quodlibet.qltk.ccb import ConfigCheckMenuItem
from quodlibet.qltk.songlist import SongList, DND_QL, DND_URI_LIST
from quodlibet.qltk.songsmenu import SongsMenu
from quodlibet.qltk.menubutton import SmallMenuButton
from quodlibet.qltk.songmodel import PlaylistModel
from quodlibet.qltk.playorder import OrderInOrder, OrderShuffle
from quodlibet.qltk.x import ScrolledWindow, SymbolicIconImage, \
    SmallImageButton, SmallImageToggleButton, MenuItem, RadioMenuItem
from quodlibet.qltk.songlistcolumns import CurrentColumn

QUEUE = os.path.join(quodlibet.get_user_dir(), "queue")


class PlaybackStatusIcon(Gtk.Box):
    """A widget showing a play/pause/stop symbolic icon"""
    def __init__(self):
        super().__init__()
        self._icons = {}

    def _set(self, name):
        if name not in self._icons:
            image = SymbolicIconImage(name, Gtk.IconSize.MENU)
            self._icons[name] = image
            image.show()
        else:
            image = self._icons[name]
Esempio n. 35
0
class PlaylistsBrowser(Browser, DisplayPatternMixin):
    name = _("Playlists")
    accelerated_name = _("_Playlists")
    keys = ["Playlists", "PlaylistsBrowser"]
    priority = 2
    replaygain_profiles = ["track"]
    __last_render = None
    _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "playlist_pattern")
    _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT

    def __init__(self,
                 songs_lib: SongFileLibrary,
                 Confirmer=ConfirmationPrompt):
        super().__init__(spacing=6)
        self._lists = ObjectModelSort(model=ObjectStore())
        self._lists.set_default_sort_func(ObjectStore._sort_on_value)

        self.songs_lib = songs_lib
        try:
            self.pl_lib: PlaylistLibrary = songs_lib.playlists
        except (AttributeError, TypeError):
            print_w("No playlist library available")

        model = self._lists.get_model()
        print_d(f"Reading playlists from library: {self.pl_lib}")
        for playlist in self.pl_lib:
            model.append(row=[playlist])

        # this is instanced with the necessary gtkdialog-settings, and afterwards
        # its run-method is called to get a to-be-compared Gtk.ResponseType
        self.Confirmer = Confirmer
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.__render = self.__create_cell_renderer()
        self.__view = view = self.__create_playlists_view(self.__render)
        self.__embed_in_scrolledwin(view)
        self.__configure_buttons(songs_lib)
        self.__configure_dnd(view)
        self.__connect_signals(view)
        self._sb_box = self.__create_searchbar(songs_lib)
        self._rh_box = None
        self._main_box = self.__create_box()
        self.show_all()

        for child in self.get_children():
            child.show_all()

        self._ids = [
            self.pl_lib.connect('removed', self.__removed),
            self.pl_lib.connect('added', self.__added),
            self.pl_lib.connect('changed', self.__changed),
        ]
        print_d(
            f"Connected signals: {self._ids} from {self.pl_lib!r} for {self}")
        self.connect("destroy", self._destroy)

    def _destroy(self, _browser):
        for id_ in self._ids:
            self.pl_lib.disconnect(id_)
        del self._ids

    def pack(self, songpane):
        self._main_box.pack1(self, True, False)
        self._rh_box = rhbox = Gtk.VBox(spacing=6)
        align = Align(self._sb_box, left=0, right=6, top=6)
        rhbox.pack_start(align, False, True, 0)
        rhbox.pack_start(songpane, True, True, 0)
        self._main_box.pack2(rhbox, True, False)
        rhbox.show()
        align.show_all()
        return self._main_box

    def unpack(self, container, songpane):
        self._rh_box.remove(songpane)
        container.remove(self._rh_box)
        container.remove(self)

    @classmethod
    def init(klass, library):
        klass.load_pattern()

    def playlists(self):
        return [row[0] for row in self._lists]

    def changed(self, playlist, refresh=True):
        for row in self._lists:
            if row[0] is playlist:
                if refresh:
                    # Changes affect aggregate caches etc
                    print_d(f"Refreshing view in {self} for {playlist}")
                    self._lists.row_changed(row.path, row.iter)
                    if playlist == self._selected_playlist():
                        print_d(f"Updating songslist for selected {playlist}")
                        self.songs_selected(playlist.songs)
                break

    def __removed(self, lib, playlists):
        for row in self.model:
            pl = row[0]
            if pl in playlists:
                print_d(f"Removing {pl} from view", str(self))
                self.__playlist_deleted(row)
        self.activate()

    def __added(self, lib, playlists):
        for playlist in playlists:
            print_d(f"Looks like a new playlist: {playlist}")
            self.model.append(row=[playlist])

    def __changed(self, lib, playlists):
        for playlist in playlists:
            self.changed(playlist)

    def cell_data(self, col, cell, model, iter, data):
        playlist = model[iter][0]
        cell.markup = markup = self.display_pattern % playlist
        if self.__last_render == markup:
            return
        self.__last_render = markup
        cell.markup = markup
        cell.set_property('markup', markup)

    def Menu(self, songs, library, items):
        model, iters = self.__get_selected_songs()
        remove = qltk.MenuItem(_("_Remove from Playlist"), Icons.LIST_REMOVE)
        qltk.add_fake_accel(remove, "Delete")
        connect_obj(remove, 'activate', self.__remove_songs, iters, model)
        playlist_iter = self.__selected_playlists()[1]
        remove.set_sensitive(bool(playlist_iter))
        items.append([remove])
        menu = super().Menu(songs, library, items)
        return menu

    def __get_selected_songs(self):
        songlist = qltk.get_top_parent(self).songlist
        model, rows = songlist.get_selection().get_selected_rows()
        iters = map(model.get_iter, rows)
        return model, iters

    @property
    def _query(self):
        return self._sb_box.get_query(SongList.star)

    def __destroy(self, *args):
        del self._sb_box

    def __create_box(self):
        box = qltk.ConfigRHPaned("browsers", "playlistsbrowser_pos", 0.4)
        box.show_all()
        return box

    def __create_searchbar(self, library):
        self.accelerators = Gtk.AccelGroup()
        completion = LibraryTagCompletion(library.librarian)
        sbb = SearchBarBox(completion=completion,
                           accel_group=self.accelerators)
        sbb.connect('query-changed', self.__text_parse)
        sbb.connect('focus-out', self.__focus)
        return sbb

    def __embed_in_scrolledwin(self, view):
        swin = ScrolledWindow()
        swin.set_shadow_type(Gtk.ShadowType.IN)
        swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        swin.add(view)
        self.pack_start(swin, True, True, 0)

    def __configure_buttons(self, library):
        new_pl = qltk.Button(None, Icons.DOCUMENT_NEW, Gtk.IconSize.MENU)
        new_pl.set_tooltip_text(_("New"))
        new_pl.connect('clicked', self.__new_playlist, library)
        import_pl = qltk.Button(None, Icons.LIST_ADD, Gtk.IconSize.MENU)
        import_pl.set_tooltip_text(_("Import"))
        import_pl.connect('clicked', self.__import, library)

        fb = Gtk.FlowBox()
        fb.set_selection_mode(Gtk.SelectionMode.NONE)
        fb.set_homogeneous(True)
        fb.insert(new_pl, 0)
        fb.insert(import_pl, 1)
        fb.set_max_children_per_line(2)

        # The pref button is in its own flowbox instead of directly under the
        # HBox to make it the same height as the other buttons
        pref = PreferencesButton(self)
        fb2 = Gtk.FlowBox()
        fb2.insert(pref, 0)

        hb = Gtk.HBox()
        hb.pack_start(fb, True, True, 0)
        hb.pack_start(fb2, False, False, 0)
        self.pack_start(hb, False, False, 0)

    def __create_playlists_view(self, render):
        view = RCMHintedTreeView()
        view.set_enable_search(True)
        view.set_search_column(0)
        view.set_search_equal_func(
            lambda model, col, key, iter, data: not model[iter][col].name.
            lower().startswith(key.lower()), None)
        col = Gtk.TreeViewColumn("Playlists", render)
        col.set_cell_data_func(render, self.cell_data)
        view.append_column(col)
        view.set_model(self._lists)
        view.set_rules_hint(True)
        view.set_headers_visible(False)
        return view

    def __configure_dnd(self, view):
        targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP,
                    DND_QL), ("text/uri-list", 0, DND_URI_LIST),
                   ("text/x-moz-url", 0, DND_MOZ_URL)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]
        view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        view.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets[:2],
                             Gdk.DragAction.COPY)
        view.connect('drag-data-received', self.__drag_data_received)
        view.connect('drag-data-get', self._drag_data_get)
        view.connect('drag-motion', self.__drag_motion)
        view.connect('drag-leave', self.__drag_leave)

    def __connect_signals(self, view):
        view.connect('row-activated', lambda *x: self.songs_activated())
        view.connect('popup-menu', self.__popup_menu, self.songs_lib)
        view.get_selection().connect('changed', self.activate)
        self.connect('key-press-event', self.__key_pressed)

    def __create_cell_renderer(self):
        render = Gtk.CellRendererText()
        render.set_padding(3, 3)
        render.set_property('ellipsize', Pango.EllipsizeMode.END)
        render.connect('editing-started', self.__start_editing)
        render.connect('edited', self.__edited)
        return render

    def key_pressed(self, event):
        if qltk.is_accel(event, "Delete"):
            self.__handle_songlist_delete()
            return True
        return False

    def __handle_songlist_delete(self, *args):
        model, iters = self.__get_selected_songs()
        self.__remove_songs(iters, model)

    def __key_pressed(self, widget, event):
        if qltk.is_accel(event, "Delete"):
            model, iter = self.__selected_playlists()
            if not iter:
                return False

            playlist = model[iter][0]
            if confirm_remove_playlist_dialog_invoke(self, playlist,
                                                     self.Confirmer):
                playlist.delete()
            else:
                print_d("Playlist removal cancelled through prompt")
            return True
        elif qltk.is_accel(event, "F2"):
            model, iter = self.__selected_playlists()
            if iter:
                self._start_rename(model.get_path(iter))
            return True
        elif qltk.is_accel(event, "<Primary>I"):
            songs = self._get_playlist_songs()
            if songs:
                window = Information(self.songs_lib.librarian, songs, self)
                window.show()
            return True
        elif qltk.is_accel(event, "<Primary>Return", "<Primary>KP_Enter"):
            qltk.enqueue(self._get_playlist_songs())
            return True
        elif qltk.is_accel(event, "<alt>Return"):
            songs = self._get_playlist_songs()
            if songs:
                window = SongProperties(self.songs_lib.librarian, songs, self)
                window.show()
            return True
        return False

    def __playlist_deleted(self, row) -> None:
        self.model.remove(row.iter)

    def __drag_motion(self, view, ctx, x, y, time):
        targets = [t.name() for t in ctx.list_targets()]
        if "text/x-quodlibet-songs" in targets:
            view.set_drag_dest(x, y, into_only=True)
            return True
        else:
            # Highlighting the view itself doesn't work.
            view.get_parent().drag_highlight()
            return True

    def __drag_leave(self, view, ctx, time):
        view.get_parent().drag_unhighlight()

    def __remove_songs(self, iters, smodel):
        def song_at(itr):
            return smodel[smodel.get_path(itr)][0]

        def remove_from_model(iters, smodel):
            for it in iters:
                smodel.remove(it)

        model, iter = self.__selected_playlists()
        if iter:
            playlist = model[iter][0]
            # Build a {iter: song} dict, exhausting `iters` once.
            removals = {
                iter_remove: song_at(iter_remove)
                for iter_remove in iters
            }
            if not removals:
                print_w("No songs selected to remove")
                return
            if self._query is None or not self.get_filter_text():
                # Calling playlist.remove_songs(songs) won't remove the
                # right ones if there are duplicates
                remove_from_model(removals.keys(), smodel)
                self.__rebuild_playlist_from_songs_model(playlist, smodel)
                # Emit manually
                self.songs_lib.emit('changed', removals.values())
            else:
                playlist.remove_songs(removals.values(), True)
                remove_from_model(removals.keys(), smodel)
            print_d("Removed %d song(s) from %s" % (len(removals), playlist))

    def __rebuild_playlist_from_songs_model(self, playlist, smodel):
        self.pl_lib.recreate(playlist, [row[0] for row in smodel])

    def _selected_playlist(self) -> Optional[Playlist]:
        """The currently selected playlist's name, or None if non selected"""
        model, iter = self.__selected_playlists()
        if not iter:
            return None
        path = model.get_path(iter)
        playlist = model[path][0]
        return playlist

    def __drag_data_received(self, view, ctx, x, y, sel, tid, etime):
        # TreeModelSort doesn't support GtkTreeDragDestDrop.
        view.emit_stop_by_name('drag-data-received')
        model = view.get_model()
        if tid == DND_QL:
            filenames = qltk.selection_get_filenames(sel)
            songs = list(
                filter(None, [self.songs_lib.get(f) for f in filenames]))
            if not songs:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            try:
                path, pos = view.get_dest_row_at_pos(x, y)
            except TypeError:
                playlist = self.pl_lib.create_from_songs(songs)
                GLib.idle_add(self._select_playlist, playlist)
            else:
                playlist = model[path][0]
                playlist.extend(songs)
            # self.changed(playlist)
            Gtk.drag_finish(ctx, True, False, etime)
            # Cause a refresh to the dragged-to playlist if it is selected
            # so that the dragged (duplicate) track(s) appears
            if playlist is self._selected_playlist():
                model, plist_iter = self.__selected_playlists()
                songlist = qltk.get_top_parent(self).songlist
                self.activate(resort=not songlist.is_sorted())
        else:
            if tid == DND_URI_LIST:
                uri = sel.get_uris()[0]
                name = os.path.basename(uri)
            elif tid == DND_MOZ_URL:
                data = sel.get_data()
                uri, name = data.decode('utf16', 'replace').split('\n')
            else:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            name = _name_for(name or os.path.basename(uri))
            try:
                sock = urlopen(uri)
                uri = uri.lower()
                if uri.endswith('.pls'):
                    playlist = parse_pls(sock,
                                         name,
                                         songs_lib=self.songs_lib,
                                         pl_lib=self.pl_lib)
                elif uri.endswith('.m3u') or uri.endswith('.m3u8'):
                    playlist = parse_m3u(sock,
                                         name,
                                         songs_lib=self.songs_lib,
                                         pl_lib=self.pl_lib)
                else:
                    raise IOError
                self.songs_lib.add(playlist.songs)
                # TODO: change to use playlist library too?
                # self.changed(playlist)
                Gtk.drag_finish(ctx, True, False, etime)
            except IOError:
                Gtk.drag_finish(ctx, False, False, etime)
                qltk.ErrorMessage(
                    qltk.get_top_parent(self), _("Unable to import playlist"),
                    _("Quod Libet can only import playlists in the M3U/M3U8 "
                      "and PLS formats.")).run()

    def _drag_data_get(self, view, ctx, sel, tid, etime):
        model, iters = self.__view.get_selection().get_selected_rows()
        songs = []
        for itr in iters:
            if itr:
                songs += model[itr][0].songs
        if tid == 0:
            qltk.selection_set_songs(sel, songs)
        else:
            sel.set_uris([song("~uri") for song in songs])

    def _select_playlist(self, playlist, scroll=False):
        view = self.__view
        model = view.get_model()
        for row in model:
            if row[0] is playlist:
                view.get_selection().select_iter(row.iter)
                if scroll:
                    view.scroll_to_cell(row.path,
                                        use_align=True,
                                        row_align=0.5)

    def __popup_menu(self, view, library):
        model, itr = view.get_selection().get_selected()
        if itr is None:
            return
        songs = list(model[itr][0])
        songs = [s for s in songs if isinstance(s, AudioFile)]
        menu = SongsMenu(library,
                         songs,
                         playlists=False,
                         remove=False,
                         ratings=False)
        menu.preseparate()

        def _remove(model, itr):
            playlist = model[itr][0]
            response = confirm_remove_playlist_dialog_invoke(
                self, playlist, self.Confirmer)
            if response:
                playlist.delete()
            else:
                print_d("Playlist removal cancelled through prompt")

        rem = MenuItem(_("_Delete"), Icons.EDIT_DELETE)
        connect_obj(rem, 'activate', _remove, model, itr)
        menu.prepend(rem)

        def _rename(path):
            self._start_rename(path)

        ren = qltk.MenuItem(_("_Rename"), Icons.EDIT)
        qltk.add_fake_accel(ren, "F2")
        connect_obj(ren, 'activate', _rename, model.get_path(itr))
        menu.prepend(ren)

        playlist = model[itr][0]
        PLAYLIST_HANDLER.populate_menu(menu, library, self, [playlist])
        menu.show_all()
        return view.popup_menu(menu, 0, Gtk.get_current_event_time())

    def _start_rename(self, path):
        view = self.__view
        self.__render.set_property('editable', True)
        view.set_cursor(path, view.get_columns()[0], start_editing=True)

    def __focus(self, widget, *args):
        qltk.get_top_parent(widget).songlist.grab_focus()

    def __text_parse(self, bar, text):
        self.activate()

    def _set_text(self, text):
        self._sb_box.set_text(text)

    def activate(self, widget=None, resort=True):
        songs = self._get_playlist_songs()
        query = self._sb_box.get_query(SongList.star)
        if query and query.is_parsable:
            songs = query.filter(songs)
        GLib.idle_add(self.songs_selected, songs, resort)

    def refresh_all(self):
        print_d("Refreshing all items...")
        model = self._lists.get_model()
        for iter_, value in model.iterrows():
            print_d(f"Refreshing row {iter_}")
            model.row_changed(model.get_path(iter_), iter_)

    @property
    def model(self):
        return self._lists.get_model()

    def _get_playlist_songs(self):
        model, iter = self.__selected_playlists()
        songs = iter and list(model[iter][0]) or []
        return [s for s in songs if isinstance(s, AudioFile)]

    def can_filter_text(self):
        return True

    def filter_text(self, text):
        self._set_text(text)
        self.activate()

    def get_filter_text(self):
        return self._sb_box.get_text()

    def can_filter(self, key):
        # TODO: special-case the ~playlists tag maybe?
        return super().can_filter(key)

    def finalize(self, restore):
        config.set("browsers", "query_text", "")

    def unfilter(self):
        self.filter_text("")

    def active_filter(self, song):
        return (song in self._get_playlist_songs()
                and (self._query is None or self._query.search(song)))

    def save(self):
        model, iter = self.__selected_playlists()
        name = iter and model[iter][0].name or ""
        config.set("browsers", "playlist", name)
        text = self.get_filter_text()
        config.set("browsers", "query_text", text)

    def __new_playlist(self, activator, library):
        playlist = self.pl_lib.create()
        self._select_playlist(playlist, scroll=True)

        model, iter = self.__selected_playlists()
        path = model.get_path(iter)
        GLib.idle_add(self._start_rename, path)

    def __start_editing(self, render, editable, path):
        editable.set_text(self._lists[path][0].name)

    def __edited(self, render, path, newname):
        return self._rename(path, newname)

    def _rename(self, path, newname):
        playlist = self._lists[path][0]
        try:
            playlist.rename(newname)
        except ValueError as s:
            qltk.ErrorMessage(None, _("Unable to rename playlist"), s).run()
        else:
            row = self._lists[path]
            child_model = self.model
            child_model.remove(self._lists.convert_iter_to_child_iter(
                row.iter))
            child_model.append(row=[playlist])
            self._select_playlist(playlist, scroll=True)

    def __import(self, activator, library):
        formats = ["*.pls", "*.m3u", "*.m3u8"]
        cf = create_chooser_filter(_("Playlists"), formats)
        fns = choose_files(self, _("Import Playlist"), _("_Import"), cf)
        self._import_playlists(fns)

    def _import_playlists(self, fns) -> Tuple[int, int]:
        """ Import m3u / pls playlists into QL
        Returns the (total playlists, total songs) added
        TODO: move this to Playlists library and watch here for new playlists
        """
        total_pls = 0
        total_songs = 0
        for filename in fns:
            name = _name_for(filename)
            with open(filename, "rb") as f:
                if filename.endswith(".m3u") or filename.endswith(".m3u8"):
                    playlist = parse_m3u(f,
                                         name,
                                         songs_lib=self.songs_lib,
                                         pl_lib=self.pl_lib)
                elif filename.endswith(".pls"):
                    playlist = parse_pls(f,
                                         name,
                                         songs_lib=self.songs_lib,
                                         pl_lib=self.pl_lib)
                else:
                    print_w("Unsupported playlist type for '%s'" % filename)
                    continue
            # Import all the songs in the playlist to the *songs* library
            total_songs += len(self.songs_lib.add(playlist))
            total_pls += 1
        return total_pls, total_songs

    def restore(self):
        try:
            name = config.get("browsers", "playlist")
        except config.Error as e:
            print_d("Couldn't get last playlist from config: %s" % e)
        else:
            self.__view.select_by_func(lambda r: r[0].name == name, one=True)
        try:
            text = config.get("browsers", "query_text")
        except config.Error as e:
            print_d("Couldn't get last search string from config: %s" % e)
        else:
            self._set_text(text)

    can_reorder = True

    def scroll(self, song):
        self.__view.iter_select_by_func(lambda r: song in r[0])

    def reordered(self, songs):
        model, iter = self.__selected_playlists()
        playlist = None
        if iter:
            playlist = model[iter][0]
            playlist[:] = songs
        elif songs:
            playlist = self.pl_lib.create_from_songs(songs)
            GLib.idle_add(self._select_playlist, playlist)
        if playlist:
            self.changed(playlist, refresh=False)

    def __selected_playlists(self):
        """Returns a tuple of (model, iter) for the current playlist(s)"""
        return self.__view.get_selection().get_selected()
Esempio n. 36
0
class PlaylistsBrowser(Browser, DisplayPatternMixin):

    name = _("Playlists")
    accelerated_name = _("_Playlists")
    keys = ["Playlists", "PlaylistsBrowser"]
    priority = 2
    replaygain_profiles = ["track"]
    __last_render = None
    _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "playlist_pattern")
    _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT

    def pack(self, songpane):
        self._main_box.pack1(self, True, False)
        self._rh_box = rhbox = Gtk.VBox(spacing=6)
        align = Align(self._sb_box, left=0, right=6, top=6)
        rhbox.pack_start(align, False, True, 0)
        rhbox.pack_start(songpane, True, True, 0)
        self._main_box.pack2(rhbox, True, False)
        rhbox.show()
        align.show_all()
        return self._main_box

    def unpack(self, container, songpane):
        self._rh_box.remove(songpane)
        container.remove(self._rh_box)
        container.remove(self)

    @classmethod
    def init(klass, library):
        klass.library = library
        model = klass.__lists.get_model()
        for playlist in os.listdir(PLAYLISTS):
            try:
                playlist = FileBackedPlaylist(
                    PLAYLISTS,
                    FileBackedPlaylist.unquote(playlist),
                    library=library)
                model.append(row=[playlist])
            except EnvironmentError:
                print_w("Invalid Playlist '%s'" % playlist)
                pass

        klass._ids = [
            library.connect('removed', klass.__removed),
            library.connect('added', klass.__added),
            library.connect('changed', klass.__changed),
        ]
        klass.load_pattern()

    @classmethod
    def deinit(cls, library):
        model = cls.__lists.get_model()
        model.clear()

        for id_ in cls._ids:
            library.disconnect(id_)
        del cls._ids

    @classmethod
    def playlists(klass):
        return [row[0] for row in klass.__lists]

    @classmethod
    def changed(klass, playlist, refresh=True):
        model = klass.__lists
        for row in model:
            if row[0] is playlist:
                if refresh:
                    print_d("Refreshing playlist %s..." % row[0])
                    klass.__lists.row_changed(row.path, row.iter)
                playlist.write()
                break
        else:
            model.get_model().append(row=[playlist])
            playlist.write()

    @classmethod
    def __removed(klass, library, songs):
        for playlist in klass.playlists():
            if playlist.remove_songs(songs):
                klass.changed(playlist)

    @classmethod
    def __added(klass, library, songs):
        filenames = {song("~filename") for song in songs}
        for playlist in klass.playlists():
            if playlist.add_songs(filenames, library):
                klass.changed(playlist)

    @classmethod
    def __changed(klass, library, songs):
        for playlist in klass.playlists():
            for song in songs:
                if song in playlist.songs:
                    klass.changed(playlist)
                    break

    def cell_data(self, col, cell, model, iter, data):
        playlist = model[iter][0]
        cell.markup = markup = self.display_pattern % playlist
        if self.__last_render == markup:
            return
        self.__last_render = markup
        cell.markup = markup
        cell.set_property('markup', markup)

    def Menu(self, songs, library, items):
        model, iters = self.__get_selected_songs()
        remove = qltk.MenuItem(_("_Remove from Playlist"), Icons.LIST_REMOVE)
        qltk.add_fake_accel(remove, "Delete")
        connect_obj(remove, 'activate', self.__remove, iters, model)
        playlist_iter = self.__view.get_selection().get_selected()[1]
        remove.set_sensitive(bool(playlist_iter))
        items.append([remove])
        menu = super(PlaylistsBrowser, self).Menu(songs, library, items)
        return menu

    def __get_selected_songs(self):
        songlist = qltk.get_top_parent(self).songlist
        model, rows = songlist.get_selection().get_selected_rows()
        iters = map(model.get_iter, rows)
        return model, iters

    __lists = ObjectModelSort(model=ObjectStore())
    __lists.set_default_sort_func(ObjectStore._sort_on_value)

    def __init__(self, library):
        self.library = library
        super(PlaylistsBrowser, self).__init__(spacing=6)
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.__render = self.__create_cell_renderer()
        self.__view = view = self.__create_playlists_view(self.__render)
        self.__embed_in_scrolledwin(view)
        self.__configure_buttons(library)
        self.__configure_dnd(view, library)
        self.__connect_signals(view, library)
        self._sb_box = self.__create_searchbar(library)
        self._main_box = self.__create_box()
        self.show_all()
        self._query = None

        for child in self.get_children():
            child.show_all()

    def __destroy(self, *args):
        del self._sb_box

    def __create_box(self):
        box = qltk.ConfigRHPaned("browsers", "playlistsbrowser_pos", 0.4)
        box.show_all()
        return box

    def __create_searchbar(self, library):
        self.accelerators = Gtk.AccelGroup()
        completion = LibraryTagCompletion(library.librarian)
        sbb = SearchBarBox(completion=completion,
                           accel_group=self.accelerators)
        sbb.connect('query-changed', self.__text_parse)
        sbb.connect('focus-out', self.__focus)
        return sbb

    def __embed_in_scrolledwin(self, view):
        swin = ScrolledWindow()
        swin.set_shadow_type(Gtk.ShadowType.IN)
        swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        swin.add(view)
        self.pack_start(swin, True, True, 0)

    def __configure_buttons(self, library):
        new_pl = qltk.Button(_("_New"), Icons.DOCUMENT_NEW, Gtk.IconSize.MENU)
        new_pl.connect('clicked', self.__new_playlist)
        import_pl = qltk.Button(_("_Import"), Icons.LIST_ADD,
                                Gtk.IconSize.MENU)
        import_pl.connect('clicked', self.__import, library)
        hb = Gtk.HBox(spacing=6)
        hb.set_homogeneous(False)
        hb.pack_start(new_pl, True, True, 0)
        hb.pack_start(import_pl, True, True, 0)
        hb2 = Gtk.HBox(spacing=0)
        hb2.pack_start(hb, True, True, 0)
        hb2.pack_start(PreferencesButton(self), False, False, 6)
        self.pack_start(Align(hb2, left=3, bottom=3), False, False, 0)

    def __create_playlists_view(self, render):
        view = RCMHintedTreeView()
        view.set_enable_search(True)
        view.set_search_column(0)
        view.set_search_equal_func(
            lambda model, col, key, iter, data: not model[iter][col].name.
            lower().startswith(key.lower()), None)
        col = Gtk.TreeViewColumn("Playlists", render)
        col.set_cell_data_func(render, self.cell_data)
        view.append_column(col)
        view.set_model(self.__lists)
        view.set_rules_hint(True)
        view.set_headers_visible(False)
        return view

    def __configure_dnd(self, view, library):
        targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP,
                    DND_QL), ("text/uri-list", 0, DND_URI_LIST),
                   ("text/x-moz-url", 0, DND_MOZ_URL)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]
        view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        view.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets[:2],
                             Gdk.DragAction.COPY)
        view.connect('drag-data-received', self.__drag_data_received, library)
        view.connect('drag-data-get', self.__drag_data_get)
        view.connect('drag-motion', self.__drag_motion)
        view.connect('drag-leave', self.__drag_leave)

    def __connect_signals(self, view, library):
        view.connect('row-activated', lambda *x: self.songs_activated())
        view.connect('popup-menu', self.__popup_menu, library)
        view.get_selection().connect('changed', self.activate)
        model = view.get_model()
        s = model.connect('row-changed', self.__check_current)
        connect_obj(self, 'destroy', model.disconnect, s)
        self.connect('key-press-event', self.__key_pressed)

    def __create_cell_renderer(self):
        render = Gtk.CellRendererText()
        render.set_padding(3, 3)
        render.set_property('ellipsize', Pango.EllipsizeMode.END)
        render.connect('editing-started', self.__start_editing)
        render.connect('edited', self.__edited)
        return render

    def key_pressed(self, event):
        if qltk.is_accel(event, "Delete"):
            self.__handle_songlist_delete()
            return True
        return False

    def __handle_songlist_delete(self, *args):
        model, iters = self.__get_selected_songs()
        self.__remove(iters, model)

    def __key_pressed(self, widget, event):
        if qltk.is_accel(event, "Delete"):
            model, iter = self.__view.get_selection().get_selected()
            if not iter:
                return False

            playlist = model[iter][0]
            dialog = ConfirmRemovePlaylistDialog(self, playlist)
            if dialog.run() == Gtk.ResponseType.YES:
                playlist.delete()
                model.get_model().remove(
                    model.convert_iter_to_child_iter(iter))
            return True
        elif qltk.is_accel(event, "F2"):
            model, iter = self.__view.get_selection().get_selected()
            if iter:
                self._start_rename(model.get_path(iter))
            return True
        return False

    def __check_current(self, model, path, iter):
        model, citer = self.__view.get_selection().get_selected()
        if citer and model.get_path(citer) == path:
            songlist = qltk.get_top_parent(self).songlist
            self.activate(resort=not songlist.is_sorted())

    def __drag_motion(self, view, ctx, x, y, time):
        targets = [t.name() for t in ctx.list_targets()]
        if "text/x-quodlibet-songs" in targets:
            view.set_drag_dest(x, y, into_only=True)
            return True
        else:
            # Highlighting the view itself doesn't work.
            view.get_parent().drag_highlight()
            return True

    def __drag_leave(self, view, ctx, time):
        view.get_parent().drag_unhighlight()

    def __remove(self, iters, smodel):
        def song_at(itr):
            return smodel[smodel.get_path(itr)][0]

        def remove_from_model(iters, smodel):
            for it in iters:
                smodel.remove(it)

        model, iter = self.__view.get_selection().get_selected()
        if iter:
            playlist = model[iter][0]
            removals = [song_at(iter_remove) for iter_remove in iters]
            if self._query is None or not self.get_filter_text():
                # Calling playlist.remove_songs(songs) won't remove the
                # right ones if there are duplicates
                remove_from_model(iters, smodel)
                self.__rebuild_playlist_from_songs_model(playlist, smodel)
                # Emit manually
                self.library.emit('changed', removals)
            else:
                print_d("Removing %d song(s) from %s" %
                        (len(removals), playlist))
                playlist.remove_songs(removals, True)
                remove_from_model(iters, smodel)
            self.changed(playlist)
            self.activate()

    def __rebuild_playlist_from_songs_model(self, playlist, smodel):
        playlist.inhibit = True
        playlist.clear()
        playlist.extend([row[0] for row in smodel])
        playlist.inhibit = False

    def __drag_data_received(self, view, ctx, x, y, sel, tid, etime, library):
        # TreeModelSort doesn't support GtkTreeDragDestDrop.
        view.emit_stop_by_name('drag-data-received')
        model = view.get_model()
        if tid == DND_QL:
            filenames = qltk.selection_get_filenames(sel)
            songs = filter(None, map(library.get, filenames))
            if not songs:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            try:
                path, pos = view.get_dest_row_at_pos(x, y)
            except TypeError:
                playlist = FileBackedPlaylist.from_songs(
                    PLAYLISTS, songs, library)
                GLib.idle_add(self._select_playlist, playlist)
            else:
                playlist = model[path][0]
                playlist.extend(songs)
            self.changed(playlist)
            Gtk.drag_finish(ctx, True, False, etime)
        else:
            if tid == DND_URI_LIST:
                uri = sel.get_uris()[0]
                name = os.path.basename(uri)
            elif tid == DND_MOZ_URL:
                data = sel.get_data()
                uri, name = data.decode('utf16', 'replace').split('\n')
            else:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            name = name or os.path.basename(uri) or _("New Playlist")
            uri = uri.encode('utf-8')
            try:
                sock = urllib.urlopen(uri)
                f = NamedTemporaryFile()
                f.write(sock.read())
                f.flush()
                if uri.lower().endswith('.pls'):
                    playlist = parse_pls(f.name, library=library)
                elif uri.lower().endswith('.m3u'):
                    playlist = parse_m3u(f.name, library=library)
                else:
                    raise IOError
                library.add_filename(playlist)
                if name:
                    playlist.rename(name)
                self.changed(playlist)
                Gtk.drag_finish(ctx, True, False, etime)
            except IOError:
                Gtk.drag_finish(ctx, False, False, etime)
                qltk.ErrorMessage(
                    qltk.get_top_parent(self), _("Unable to import playlist"),
                    _("Quod Libet can only import playlists in the M3U "
                      "and PLS formats.")).run()

    def __drag_data_get(self, view, ctx, sel, tid, etime):
        model, iters = self.__view.get_selection().get_selected_rows()
        songs = []
        for iter in filter(lambda i: i, iters):
            songs += list(model[iter][0])
        if tid == 0:
            qltk.selection_set_songs(sel, songs)
        else:
            sel.set_uris([song("~uri") for song in songs])

    def _select_playlist(self, playlist, scroll=False):
        view = self.__view
        model = view.get_model()
        for row in model:
            if row[0] is playlist:
                view.get_selection().select_iter(row.iter)
                if scroll:
                    view.scroll_to_cell(row.path,
                                        use_align=True,
                                        row_align=0.5)

    def __popup_menu(self, view, library):
        model, itr = view.get_selection().get_selected()
        if itr is None:
            return
        songs = list(model[itr][0])
        songs = filter(lambda s: isinstance(s, AudioFile), songs)
        menu = SongsMenu(library,
                         songs,
                         playlists=False,
                         remove=False,
                         ratings=False)
        menu.preseparate()

        def _remove(model, itr):
            playlist = model[itr][0]
            dialog = ConfirmRemovePlaylistDialog(self, playlist)
            if dialog.run() == Gtk.ResponseType.YES:
                playlist.delete()
                model.get_model().remove(model.convert_iter_to_child_iter(itr))

        rem = MenuItem(_("_Delete"), Icons.EDIT_DELETE)
        connect_obj(rem, 'activate', _remove, model, itr)
        menu.prepend(rem)

        def _rename(path):
            self._start_rename(path)

        ren = qltk.MenuItem(_("_Rename"), Icons.EDIT)
        qltk.add_fake_accel(ren, "F2")
        connect_obj(ren, 'activate', _rename, model.get_path(itr))
        menu.prepend(ren)

        playlist = model[itr][0]
        PLAYLIST_HANDLER.populate_menu(menu, library, self, [playlist])
        menu.show_all()
        return view.popup_menu(menu, 0, Gtk.get_current_event_time())

    def _start_rename(self, path):
        view = self.__view
        self.__render.set_property('editable', True)
        view.set_cursor(path, view.get_columns()[0], start_editing=True)

    def __focus(self, widget, *args):
        qltk.get_top_parent(widget).songlist.grab_focus()

    def __text_parse(self, bar, text):
        self.activate()

    def _set_text(self, text):
        self._sb_box.set_text(text)

    def activate(self, widget=None, resort=True):
        songs = self._get_playlist_songs()

        text = self.get_filter_text()
        # TODO: remove static dependency on Query
        if Query.is_parsable(text):
            self._query = Query(text, SongList.star)
            songs = self._query.filter(songs)
        GLib.idle_add(self.songs_selected, songs, resort)

    @classmethod
    def refresh_all(cls):
        model = cls.__lists.get_model()
        for iter_, value in model.iterrows():
            model.row_changed(model.get_path(iter_), iter_)

    @property
    def model(self):
        return self.__lists.get_model()

    def _get_playlist_songs(self):
        model, iter = self.__view.get_selection().get_selected()
        songs = iter and list(model[iter][0]) or []
        songs = filter(lambda s: isinstance(s, AudioFile), songs)
        return songs

    def can_filter_text(self):
        return True

    def filter_text(self, text):
        self._set_text(text)
        self.activate()

    def get_filter_text(self):
        return self._sb_box.get_text()

    def can_filter(self, key):
        # TODO: special-case the ~playlists tag maybe?
        return super(PlaylistsBrowser, self).can_filter(key)

    def finalize(self, restore):
        config.set("browsers", "query_text", "")

    def unfilter(self):
        self.filter_text("")

    def active_filter(self, song):
        return (song in self._get_playlist_songs()
                and (self._query is None or self._query.search(song)))

    def save(self):
        model, iter = self.__view.get_selection().get_selected()
        name = iter and model[iter][0].name or ""
        config.set("browsers", "playlist", name)
        text = self.get_filter_text()
        config.set("browsers", "query_text", text)

    def __new_playlist(self, activator):
        playlist = FileBackedPlaylist.new(PLAYLISTS)
        self.model.append(row=[playlist])
        self._select_playlist(playlist, scroll=True)

        model, iter = self.__view.get_selection().get_selected()
        path = model.get_path(iter)
        GLib.idle_add(self._start_rename, path)

    def __start_editing(self, render, editable, path):
        editable.set_text(self.__lists[path][0].name)

    def __edited(self, render, path, newname):
        return self._rename(path, newname)

    def _rename(self, path, newname):
        playlist = self.__lists[path][0]
        try:
            playlist.rename(newname)
        except ValueError as s:
            qltk.ErrorMessage(None, _("Unable to rename playlist"), s).run()
        else:
            row = self.__lists[path]
            child_model = self.model
            child_model.remove(
                self.__lists.convert_iter_to_child_iter(row.iter))
            child_model.append(row=[playlist])
            self._select_playlist(playlist, scroll=True)

    def __import(self, activator, library):
        filt = lambda fn: fn.endswith(".pls") or fn.endswith(".m3u")
        from quodlibet.qltk.chooser import FileChooser
        chooser = FileChooser(self, _("Import Playlist"), filt, get_home_dir())
        files = chooser.run()
        chooser.destroy()
        for filename in files:
            if filename.endswith(".m3u"):
                playlist = parse_m3u(filename, library=library)
            elif filename.endswith(".pls"):
                playlist = parse_pls(filename, library=library)
            else:
                qltk.ErrorMessage(
                    qltk.get_top_parent(self), _("Unable to import playlist"),
                    _("Quod Libet can only import playlists in the M3U "
                      "and PLS formats.")).run()
                return
            self.changed(playlist)
            library.add(playlist)

    def restore(self):
        try:
            name = config.get("browsers", "playlist")
        except config.Error as e:
            print_d("Couldn't get last playlist from config: %s" % e)
        else:
            self.__view.select_by_func(lambda r: r[0].name == name, one=True)
        try:
            text = config.get("browsers", "query_text")
        except config.Error as e:
            print_d("Couldn't get last search string from config: %s" % e)
        else:
            self._set_text(text)

    can_reorder = True

    def scroll(self, song):
        self.__view.iter_select_by_func(lambda r: song in r[0])

    def reordered(self, songs):
        model, iter = self.__view.get_selection().get_selected()
        playlist = None
        if iter:
            playlist = model[iter][0]
            playlist[:] = songs
        elif songs:
            playlist = FileBackedPlaylist.from_songs(PLAYLISTS, songs)
            GLib.idle_add(self._select_playlist, playlist)
        if playlist:
            self.changed(playlist, refresh=False)
Esempio n. 37
0
class ExceptionDialog(Gtk.Window):
    """The windows which is shown if an unhandled exception occurred"""

    _running = False
    _instance = None

    DUMPDIR = os.path.join(quodlibet.get_user_dir(), "dumps")

    @classmethod
    def from_except(cls, type_, value, traceback):
        """Returns an instance or None."""

        # Don't get in a recursive exception handler loop.
        if not cls._running:
            cls._running = True
            cls._instance = cls(type_, value, traceback)
            return cls._instance

    @property
    def dump_path(self):
        return os.path.join(
            self.DUMPDIR, time.strftime("Dump_%Y%m%d_%H%M%S.txt", self._time))

    @property
    def minidump_path(self):
        return os.path.join(
            self.DUMPDIR,
            time.strftime("MiniDump_%Y%m%d_%H%M%S.txt", self._time))

    def dump_to_disk(self, type_, value, traceback):
        """Writes the dump files to DUMDIR"""

        mkdir(self.DUMPDIR)

        header = format_dump_header(type_, value, traceback).encode("utf-8")
        log = format_dump_log().encode("utf-8")

        print(self.dump_path)
        with open(self.dump_path, "wb") as dump:
            with open(self.minidump_path, "wb") as minidump:
                minidump.write(header)
                dump.write(header)
            dump.write(log)

    def __init__(self, type_, value, traceback):
        # This is all implemented a bit different than the rest of Quod
        # Libet's windows since I want it to be as stupid as possible, to
        # minimize the chances of something going wrong with the thing
        # that handles things going wrong, i.e. it only uses GTK+ code,
        # no QLTK wrappers.

        self._time = time.localtime()

        Gtk.Window.__init__(self)
        self.set_default_size(400, 400)
        self.set_border_width(12)
        self.set_title(_("Error Occurred"))

        desc = _(
            "An exception has occured in Quod Libet. A dump file has "
            "been saved to <b >%(dump-path)s</b> that will help us debug the "
            "crash. "
            "Please file a new issue at %(new-issue-url)s"
            "and attach this file or include its contents. This "
            "file may contain some identifying information about you or your "
            "system, such as a list of recent files played. If this is "
            "unacceptable, send <b>%(mini-dump-path)s</b> instead with a "
            "description of what "
            "you were doing.") % {
                "dump-path": unexpand(self.dump_path),
                "mini-dump-path": unexpand(self.minidump_path),
                "new-issue-url":
                "https://github.com/quodlibet/quodlibet/issues/new",
            }

        suggestion = _(
            "Quod Libet may now be unstable. Closing it and "
            "restarting is recommended. Your library will be saved.")

        label = Gtk.Label(label=desc + "\n\n" + suggestion)

        label.set_selectable(True)
        label.set_use_markup(True)
        label.set_line_wrap(True)
        box = Gtk.VBox(spacing=6)
        buttons = Gtk.HButtonBox()

        viewbox = Gtk.VBox(spacing=4)
        buf = Gtk.TextBuffer()
        buf.set_text(text_type(value))
        viewbox.add(Gtk.TextView(buffer=buf, editable=False))
        view = Gtk.TreeView()
        viewbox.add(view)
        view.set_headers_visible(False)
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
        sw.set_shadow_type(Gtk.ShadowType.IN)
        sw.add(viewbox)
        model = Gtk.ListStore(object, object, object)
        self.__fill_list(view, model, value, traceback)
        view.set_model(model)
        cancel = qltk.Button(_("_Cancel"))
        close = qltk.Button(_("_Quit"), Icons.APPLICATION_EXIT)
        buttons.pack_start(close, True, True, 0)
        buttons.pack_start(cancel, True, True, 0)
        box.pack_start(label, False, True, 0)
        box.pack_start(sw, True, True, 0)
        box.pack_start(buttons, False, True, 0)
        self.add(box)

        self.connect('destroy', self.__destroy)
        connect_obj(cancel, 'clicked', Gtk.Window.destroy, self)
        close.connect('clicked', lambda *x: Gtk.main_quit())

        self.get_child().show_all()

        def first_draw(*args):
            filename = unexpand(self.dump_path)
            offset = gdecode(label.get_text()).find(filename)
            label.select_region(offset, offset + len(filename))
            self.disconnect(self.__draw_id)

        self.__draw_id = self.connect("draw", first_draw)

    def __stack_row_activated(self, view, path, column):
        model = view.get_model()
        filename = model[path][0]
        line = model[path][2]
        util.spawn(["sensible-editor", "+%d" % line, filename])

    def __fill_list(self, view, model, value, trace):
        for frame in reversed(extract_tb(trace)):
            (filename, line, function, text) = frame
            model.append(row=[filename, function, line])
        view.connect('row-activated', self.__stack_row_activated)

        def cdf(column, cell, model, iter, data):
            row = model[iter]
            filename = fsn2text(unexpand(row[0]))
            function = row[1]
            line = row[2]
            cell.set_property(
                "markup", "<b>%s</b> line %d\n\t%s" %
                (util.escape(function), line, util.escape(filename)))

        render = Gtk.CellRendererText()
        col = Gtk.TreeViewColumn(u"", render)
        col.set_cell_data_func(render, cdf)
        col.set_visible(True)
        col.set_expand(True)
        view.append_column(col)

    def __destroy(self, window):
        type(self)._running = False
        type(self).instance = None
        window.destroy()
Esempio n. 38
0
class CoverGrid(Browser, util.InstanceTracker, VisibleUpdate,
                DisplayPatternMixin):
    __gsignals__ = Browser.__gsignals__
    __model = None
    __last_render = None
    __last_render_surface = None

    _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "album_pattern")
    _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT
    STAR = ["~people", "album"]

    name = _("Cover Grid")
    accelerated_name = _("_Cover Grid")
    keys = ["CoverGrid"]
    priority = 5

    def pack(self, songpane):
        container = self.songcontainer
        container.pack1(self, True, False)
        container.pack2(songpane, True, False)
        return container

    def unpack(self, container, songpane):
        container.remove(songpane)
        container.remove(self)

    @classmethod
    def init(klass, library):
        super(CoverGrid, klass).load_pattern()

    def finalize(self, restored):
        if not restored:
            # Select the "All Albums" album, which is None
            self.select_by_func(lambda r: r[0].album is None, one=True)

    @classmethod
    def _destroy_model(klass):
        klass.__model.destroy()
        klass.__model = None

    @classmethod
    def toggle_text(klass):
        on = config.getboolean("browsers", "album_text", True)
        for covergrid in klass.instances():
            covergrid.__text_cells.set_visible(on)
            covergrid.view.queue_resize()

    @classmethod
    def toggle_wide(klass):
        wide = config.getboolean("browsers", "covergrid_wide", False)
        for covergrid in klass.instances():
            covergrid.songcontainer.set_orientation(
                Gtk.Orientation.HORIZONTAL if wide
                else Gtk.Orientation.VERTICAL)

    @classmethod
    def update_mag(klass):
        mag = config.getfloat("browsers", "covergrid_magnification", 3.)
        for covergrid in klass.instances():
            covergrid.__cover.set_property('width', get_cover_size() * mag + 8)
            covergrid.__cover.set_property('height',
                get_cover_size() * mag + 8)
            covergrid.view.set_item_width(get_cover_size() * mag + 8)
            covergrid.view.queue_resize()
            covergrid.redraw()

    def redraw(self):
        model = self.__model
        for iter_, item in model.iterrows():
            album = item.album
            if album is not None:
                item.scanned = False
                model.row_changed(model.get_path(iter_), iter_)

    @classmethod
    def _init_model(klass, library):
        klass.__model = AlbumModel(library)
        klass.__library = library

    @classmethod
    def _refresh_albums(klass, albums):
        """We signal all other open album views that we changed something
        (Only needed for the cover atm) so they redraw as well."""
        if klass.__library:
            klass.__library.albums.refresh(albums)

    @util.cached_property
    def _no_cover(self) -> Optional[cairo.Surface]:
        """Returns a cairo surface representing a missing cover"""

        mag = config.getfloat("browsers", "covergrid_magnification", 3.)

        cover_size = get_cover_size()
        scale_factor = self.get_scale_factor() * mag
        pb = get_no_cover_pixbuf(cover_size, cover_size, scale_factor)
        return get_surface_for_pixbuf(self, pb)

    def __init__(self, library):
        Browser.__init__(self, spacing=6)
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.songcontainer = qltk.paned.ConfigRVPaned(
            "browsers", "covergrid_pos", 0.4)
        if config.getboolean("browsers", "covergrid_wide", False):
            self.songcontainer.set_orientation(Gtk.Orientation.HORIZONTAL)

        self._register_instance()
        if self.__model is None:
            self._init_model(library)

        self._cover_cancel = Gio.Cancellable()

        self.scrollwin = sw = ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.IN)
        model_sort = AlbumSortModel(model=self.__model)
        model_filter = AlbumFilterModel(child_model=model_sort)
        self.view = view = IconView(model_filter)
        #view.set_item_width(get_cover_size() + 12)
        self.view.set_row_spacing(config.getint("browsers", "row_spacing", 6))
        self.view.set_column_spacing(config.getint("browsers",
            "column_spacing", 6))
        self.view.set_item_padding(config.getint("browsers",
            "item_padding", 6))
        self.view.set_has_tooltip(True)
        self.view.connect("query-tooltip", self._show_tooltip)

        self.__bg_filter = background_filter()
        self.__filter = None
        model_filter.set_visible_func(self.__parse_query)

        mag = config.getfloat("browsers", "covergrid_magnification", 3.)

        self.view.set_item_width(get_cover_size() * mag + 8)

        self.__cover = render = Gtk.CellRendererPixbuf()
        render.set_property('width', get_cover_size() * mag + 8)
        render.set_property('height', get_cover_size() * mag + 8)
        view.pack_start(render, False)

        def cell_data_pb(view, cell, model, iter_, no_cover):
            item = model.get_value(iter_)

            if item.album is None:
                surface = None
            elif item.cover:
                pixbuf = item.cover
                pixbuf = add_border_widget(pixbuf, self.view)
                surface = get_surface_for_pixbuf(self, pixbuf)
                # don't cache, too much state has an effect on the result
                self.__last_render_surface = None
            else:
                surface = no_cover

            if self.__last_render_surface == surface:
                return
            self.__last_render_surface = surface
            cell.set_property("surface", surface)

        view.set_cell_data_func(render, cell_data_pb, self._no_cover)

        self.__text_cells = render = Gtk.CellRendererText()
        render.set_visible(config.getboolean("browsers", "album_text", True))
        render.set_property('alignment', Pango.Alignment.CENTER)
        render.set_property('xalign', 0.5)
        render.set_property('ellipsize', Pango.EllipsizeMode.END)
        view.pack_start(render, False)

        def cell_data(view, cell, model, iter_, data):
            album = model.get_album(iter_)

            if album is None:
                text = "<b>%s</b>" % _("All Albums")
                text += "\n" + ngettext("%d album", "%d albums",
                        len(model) - 1) % (len(model) - 1)
                markup = text
            else:
                markup = self.display_pattern % album

            if self.__last_render == markup:
                return
            self.__last_render = markup
            cell.markup = markup
            cell.set_property('markup', markup)

        view.set_cell_data_func(render, cell_data, None)

        view.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sw.add(view)

        view.connect('item-activated', self.__play_selection, None)

        self.__sig = connect_destroy(
            view, 'selection-changed',
            util.DeferredSignal(self.__update_songs, owner=self))

        targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP, 1),
                   ("text/uri-list", 0, 2)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        view.drag_source_set(
            Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY)
        view.connect("drag-data-get", self.__drag_data_get) # NOT WORKING
        connect_obj(view, 'button-press-event',
            self.__rightclick, view, library)
        connect_obj(view, 'popup-menu', self.__popup, view, library)

        self.accelerators = Gtk.AccelGroup()
        search = SearchBarBox(completion=AlbumTagCompletion(),
                              accel_group=self.accelerators)
        search.connect('query-changed', self.__update_filter)
        connect_obj(search, 'focus-out', lambda w: w.grab_focus(), view)
        self.__search = search

        prefs = PreferencesButton(self, model_sort)
        search.pack_start(prefs, False, True, 0)
        self.pack_start(Align(search, left=6, top=6), False, True, 0)
        self.pack_start(sw, True, True, 0)

        self.connect("destroy", self.__destroy)

        self.enable_row_update(view, sw, self.view)

        self.__update_filter()

        self.connect('key-press-event', self.__key_pressed, library.librarian)

        if app.cover_manager:
            connect_destroy(
                app.cover_manager, "cover-changed", self._cover_changed)

        self.show_all()

    def _cover_changed(self, manager, songs):
        model = self.__model
        songs = set(songs)
        for iter_, item in model.iterrows():
            album = item.album
            if album is not None and songs & album.songs:
                item.scanned = False
                model.row_changed(model.get_path(iter_), iter_)

    def __key_pressed(self, widget, event, librarian):
        if qltk.is_accel(event, "<Primary>I"):
            songs = self.__get_selected_songs()
            if songs:
                window = Information(librarian, songs, self)
                window.show()
            return True
        elif qltk.is_accel(event, "<Primary>Return", "<Primary>KP_Enter"):
            qltk.enqueue(self.__get_selected_songs(sort=True))
            return True
        elif qltk.is_accel(event, "<alt>Return"):
            songs = self.__get_selected_songs()
            if songs:
                window = SongProperties(librarian, songs, self)
                window.show()
            return True
        return False

    def _row_needs_update(self, model, iter_):
        item = model.get_value(iter_)
        return item.album is not None and not item.scanned

    def _update_row(self, filter_model, iter_):
        sort_model = filter_model.get_model()
        model = sort_model.get_model()
        iter_ = filter_model.convert_iter_to_child_iter(iter_)
        iter_ = sort_model.convert_iter_to_child_iter(iter_)
        tref = Gtk.TreeRowReference.new(model, model.get_path(iter_))
        mag = config.getfloat("browsers", "covergrid_magnification", 3.)

        def callback():
            path = tref.get_path()
            if path is not None:
                model.row_changed(path, model.get_iter(path))
            # XXX: icon view seems to ignore row_changed signals for pixbufs..
            self.queue_draw()

        item = model.get_value(iter_)
        scale_factor = self.get_scale_factor() * mag
        item.scan_cover(scale_factor=scale_factor,
                        callback=callback,
                        cancel=self._cover_cancel)

    def __destroy(self, browser):
        self._cover_cancel.cancel()
        self.disable_row_update()

        self.view.set_model(None)

        klass = type(browser)
        if not klass.instances():
            klass._destroy_model()

    def __update_filter(self, entry=None, text=None, scroll_up=True,
                        restore=False):
        model = self.view.get_model()

        self.__filter = None
        query = self.__search.get_query(self.STAR)
        if not query.matches_all:
            self.__filter = query.search
        self.__bg_filter = background_filter()

        self.__inhibit()

        # If we're hiding "All Albums", then there will always
        # be something to filter ­— probably there's a better
        # way to implement this

        if (not restore or self.__filter or self.__bg_filter) or (not
            config.getboolean("browsers", "covergrid_all", True)):
            model.refilter()

        self.__uninhibit()

    def __parse_query(self, model, iter_, data):
        f, b = self.__filter, self.__bg_filter
        album = model.get_album(iter_)

        if f is None and b is None and album is not None:
            return True
        else:
            if album is None:
                return config.getboolean("browsers", "covergrid_all", True)
            elif b is None:
                return f(album)
            elif f is None:
                return b(album)
            else:
                return b(album) and f(album)

    def __search_func(self, model, column, key, iter_, data):
        album = model.get_album(iter_)
        if album is None:
            return config.getboolean("browsers", "covergrid_all", True)
        key = key.lower()
        title = album.title.lower()
        if key in title:
            return False
        if config.getboolean("browsers", "album_substrings"):
            people = (p.lower() for p in album.list("~people"))
            for person in people:
                if key in person:
                    return False
        return True

    def __rightclick(self, view, event, library):
        x = int(event.x)
        y = int(event.y)
        current_path = view.get_path_at_pos(x, y)
        if event.button == Gdk.BUTTON_SECONDARY and current_path:
            if not view.path_is_selected(current_path):
                view.unselect_all()
            view.select_path(current_path)
            self.__popup(view, library)

    def __popup(self, view, library):

        albums = self.__get_selected_albums()
        songs = self.__get_songs_from_albums(albums)

        items = []
        num = len(albums)
        button = MenuItem(
            ngettext("Reload album _cover", "Reload album _covers", num),
            Icons.VIEW_REFRESH)
        button.connect('activate', self.__refresh_album, view)
        items.append(button)

        menu = SongsMenu(library, songs, items=[items])
        menu.show_all()
        popup_menu_at_widget(menu, view,
            Gdk.BUTTON_SECONDARY,
            Gtk.get_current_event_time())

    def _show_tooltip(self, widget, x, y, keyboard_tip, tooltip):
        w = self.scrollwin.get_hadjustment().get_value()
        z = self.scrollwin.get_vadjustment().get_value()
        path = widget.get_path_at_pos(int(x + w), int(y + z))
        if path is None:
            return False
        model = widget.get_model()
        iter = model.get_iter(path)
        album = model.get_album(iter)
        if album is None:
            text = "<b>%s</b>" % _("All Albums")
            text += "\n" + ngettext("%d album",
                "%d albums", len(model) - 1) % (len(model) - 1)
            markup = text
        else:
            markup = self.display_pattern % album
        tooltip.set_markup(markup)
        return True

    def __refresh_album(self, menuitem, view):
        items = self.__get_selected_items()
        for item in items:
            item.scanned = False
        model = self.view.get_model()
        for iter_, item in model.iterrows():
            if item in items:
                model.row_changed(model.get_path(iter_), iter_)

    def __get_selected_items(self):
        model = self.view.get_model()
        paths = self.view.get_selected_items()
        return model.get_items(paths)

    def __get_selected_albums(self):
        model = self.view.get_model()
        paths = self.view.get_selected_items()
        return model.get_albums(paths)

    def __get_songs_from_albums(self, albums, sort=True):
        # Sort first by how the albums appear in the model itself,
        # then within the album using the default order.
        songs = []
        if sort:
            for album in albums:
                songs.extend(sorted(album.songs, key=lambda s: s.sort_key))
        else:
            for album in albums:
                songs.extend(album.songs)
        return songs

    def __get_selected_songs(self, sort=True):
        albums = self.__get_selected_albums()
        return self.__get_songs_from_albums(albums, sort)

    def __drag_data_get(self, view, ctx, sel, tid, etime):
        songs = self.__get_selected_songs()
        if tid == 1:
            qltk.selection_set_songs(sel, songs)
        else:
            sel.set_uris([song("~uri") for song in songs])

    def __play_selection(self, view, indices, col):
        self.songs_activated()

    def active_filter(self, song):
        for album in self.__get_selected_albums():
            if song in album.songs:
                return True
        return False

    def can_filter_text(self):
        return True

    def filter_text(self, text):
        self.__search.set_text(text)
        if Query(text).is_parsable:
            self.__update_filter(self.__search, text)
            # self.__inhibit()
            #self.view.set_cursor((0,), None, False)
            # self.__uninhibit()
            self.activate()

    def get_filter_text(self):
        return self.__search.get_text()

    def can_filter(self, key):
        # Numerics are different for collections, and although title works,
        # it's not of much use here.
        if key is not None and (key.startswith("~#") or key == "title"):
            return False
        return super().can_filter(key)

    def can_filter_albums(self):
        return True

    def list_albums(self):
        model = self.view.get_model()
        return [row[0].album.key for row in model if row[0].album]

    def select_by_func(self, func, scroll=True, one=False):
        model = self.view.get_model()
        if not model:
            return False

        selection = self.view.get_selected_items()
        first = True
        for row in model:
            if func(row):
                if not first:
                    selection.select_path(row.path)
                    continue
                self.view.unselect_all()
                self.view.select_path(row.path)
                self.view.set_cursor(row.path, None, False)
                if scroll:
                    self.view.scroll_to_path(row.path, True, 0.5, 0.5)
                first = False
                if one:
                    break
        return not first

    def filter_albums(self, values):
        self.__inhibit()
        changed = self.select_by_func(
            lambda r: r[0].album and r[0].album.key in values)
        self.view.grab_focus()
        self.__uninhibit()
        if changed:
            self.activate()

    def unfilter(self):
        self.filter_text("")

    def activate(self):
        self.view.emit('selection-changed')

    def __inhibit(self):
        self.view.handler_block(self.__sig)

    def __uninhibit(self):
        self.view.handler_unblock(self.__sig)

    def restore(self):
        text = config.gettext("browsers", "query_text")
        entry = self.__search
        entry.set_text(text)

        # update_filter expects a parsable query
        if Query(text).is_parsable:
            self.__update_filter(entry, text, scroll_up=False, restore=True)

        keys = config.gettext("browsers", "covergrid", "").split("\n")

        self.__inhibit()
        if keys != [""]:
            def select_fun(row):
                album = row[0].album
                if not album:  # all
                    return False
                return album.str_key in keys
            self.select_by_func(select_fun)
        else:
            self.select_by_func(lambda r: r[0].album is None)
        self.__uninhibit()

    def scroll(self, song):
        album_key = song.album_key
        select = lambda r: r[0].album and r[0].album.key == album_key
        self.select_by_func(select, one=True)

    def __get_config_string(self):
        model = self.view.get_model()
        paths = self.view.get_selected_items()

        # All is selected
        if model.contains_all(paths):
            return ""

        # All selected albums
        albums = model.get_albums(paths)

        confval = "\n".join((a.str_key for a in albums))
        # ConfigParser strips a trailing \n so we move it to the front
        if confval and confval[-1] == "\n":
            confval = "\n" + confval[:-1]
        return confval

    def save(self):
        conf = self.__get_config_string()
        config.settext("browsers", "covergrid", conf)
        text = self.__search.get_text()
        config.settext("browsers", "query_text", text)

    def __update_songs(self, selection):
        songs = self.__get_selected_songs(sort=False)
        self.songs_selected(songs)
Esempio n. 39
0
class AlbumList(Browser, util.InstanceTracker, VisibleUpdate,
                DisplayPatternMixin):
    __model = None
    __last_render = None
    __last_render_surface = None

    _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "album_pattern")
    _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT

    name = _("Album List")
    accelerated_name = _("_Album List")
    keys = ["AlbumList"]
    priority = 4

    def pack(self, songpane):
        container = qltk.ConfigRHPaned("browsers", "albumlist_pos", 0.4)
        container.pack1(self, True, False)
        container.pack2(songpane, True, False)
        return container

    def unpack(self, container, songpane):
        container.remove(songpane)
        container.remove(self)

    @classmethod
    def init(klass, library):
        super(AlbumList, klass).load_pattern()

    def finalize(self, restored):
        if not restored:
            self.view.set_cursor((0, ))

    @classmethod
    def _destroy_model(klass):
        klass.__model.destroy()
        klass.__model = None

    @classmethod
    def toggle_covers(klass):
        on = config.getboolean("browsers", "album_covers")
        for albumlist in klass.instances():
            albumlist.__cover_column.set_visible(on)
            for column in albumlist.view.get_columns():
                column.queue_resize()

    @classmethod
    def refresh_all(cls):
        cls.__model.refresh_all()

    @classmethod
    def _init_model(klass, library):
        klass.__model = AlbumModel(library)
        klass.__library = library

    @util.cached_property
    def _no_cover(self):
        """Returns a cairo surface representing a missing cover"""

        cover_size = get_cover_size()
        scale_factor = self.get_scale_factor()
        pb = get_no_cover_pixbuf(cover_size, cover_size, scale_factor)
        return get_surface_for_pixbuf(self, pb)

    def __init__(self, library):
        super(AlbumList, self).__init__(spacing=6)
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self._register_instance()
        if self.__model is None:
            self._init_model(library)

        self._cover_cancel = Gio.Cancellable()

        sw = ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.IN)
        self.view = view = AllTreeView()
        view.set_headers_visible(False)
        model_sort = AlbumSortModel(model=self.__model)
        model_filter = AlbumFilterModel(child_model=model_sort)

        self.__bg_filter = background_filter()
        self.__filter = None
        model_filter.set_visible_func(self.__parse_query)

        render = Gtk.CellRendererPixbuf()
        self.__cover_column = column = Gtk.TreeViewColumn("covers", render)
        column.set_visible(config.getboolean("browsers", "album_covers"))
        column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        column.set_fixed_width(get_cover_size() + 12)
        render.set_property('height', get_cover_size() + 8)
        render.set_property('width', get_cover_size() + 8)

        def cell_data_pb(column, cell, model, iter_, no_cover):
            item = model.get_value(iter_)

            if item.album is None:
                surface = None
            elif item.cover:
                pixbuf = item.cover
                pixbuf = add_border_widget(pixbuf, self.view)
                surface = get_surface_for_pixbuf(self, pixbuf)
                # don't cache, too much state has an effect on the result
                self.__last_render_surface = None
            else:
                surface = no_cover

            if self.__last_render_surface == surface:
                return
            self.__last_render_surface = surface
            cell.set_property("surface", surface)

        column.set_cell_data_func(render, cell_data_pb, self._no_cover)
        view.append_column(column)

        render = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn("albums", render)
        column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        if view.supports_hints():
            render.set_property('ellipsize', Pango.EllipsizeMode.END)

        def cell_data(column, cell, model, iter_, data):
            album = model.get_album(iter_)

            if album is None:
                text = "<b>%s</b>\n" % _("All Albums")
                text += numeric_phrase("%d album", "%d albums", len(model) - 1)
                markup = text
            else:
                markup = self.display_pattern % album

            if self.__last_render == markup:
                return
            self.__last_render = markup
            cell.markup = markup
            cell.set_property('markup', markup)

        column.set_cell_data_func(render, cell_data)
        view.append_column(column)

        view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
        view.set_rules_hint(True)
        view.set_search_equal_func(self.__search_func, None)
        view.set_search_column(0)
        view.set_model(model_filter)
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        sw.add(view)

        view.connect('row-activated', self.__play_selection)
        self.__sig = view.connect(
            'selection-changed',
            util.DeferredSignal(self.__update_songs, owner=view))

        targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP, 1),
                   ("text/uri-list", 0, 2)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        view.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets,
                             Gdk.DragAction.COPY)
        view.connect("drag-data-get", self.__drag_data_get)
        connect_obj(view, 'popup-menu', self.__popup, view, library)

        self.accelerators = Gtk.AccelGroup()
        search = SearchBarBox(completion=AlbumTagCompletion(),
                              accel_group=self.accelerators)
        search.connect('query-changed', self.__update_filter)
        connect_obj(search, 'focus-out', lambda w: w.grab_focus(), view)
        self.__search = search

        prefs = PreferencesButton(self, model_sort)
        search.pack_start(prefs, False, True, 0)
        self.pack_start(Align(search, left=6, top=6), False, True, 0)
        self.pack_start(sw, True, True, 0)

        self.connect("destroy", self.__destroy)

        self.enable_row_update(view, sw, self.__cover_column)

        self.connect('key-press-event', self.__key_pressed, library.librarian)

        if app.cover_manager:
            connect_destroy(app.cover_manager, "cover-changed",
                            self._cover_changed)

        self.show_all()

    def _cover_changed(self, manager, songs):
        model = self.__model
        songs = set(songs)
        for iter_, item in model.iterrows():
            album = item.album
            if album is not None and songs & album.songs:
                item.scanned = False
                model.row_changed(model.get_path(iter_), iter_)

    def __key_pressed(self, widget, event, librarian):
        if qltk.is_accel(event, "<Primary>I"):
            songs = self.__get_selected_songs()
            if songs:
                window = Information(librarian, songs, self)
                window.show()
            return True
        elif qltk.is_accel(event, "<Primary>Return", "<Primary>KP_Enter"):
            qltk.enqueue(self.__get_selected_songs(sort=True))
            return True
        elif qltk.is_accel(event, "<alt>Return"):
            songs = self.__get_selected_songs()
            if songs:
                window = SongProperties(librarian, songs, self)
                window.show()
            return True
        return False

    def _row_needs_update(self, model, iter_):
        item = model.get_value(iter_)
        return item.album is not None and not item.scanned

    def _update_row(self, filter_model, iter_):
        sort_model = filter_model.get_model()
        model = sort_model.get_model()
        iter_ = filter_model.convert_iter_to_child_iter(iter_)
        iter_ = sort_model.convert_iter_to_child_iter(iter_)
        tref = Gtk.TreeRowReference.new(model, model.get_path(iter_))

        def callback():
            path = tref.get_path()
            if path is not None:
                model.row_changed(path, model.get_iter(path))

        item = model.get_value(iter_)
        scale_factor = self.get_scale_factor()
        item.scan_cover(scale_factor=scale_factor,
                        callback=callback,
                        cancel=self._cover_cancel)

    def __destroy(self, browser):
        self._cover_cancel.cancel()
        self.disable_row_update()

        self.view.set_model(None)

        klass = type(browser)
        if not klass.instances():
            klass._destroy_model()

    def __update_filter(self, entry, text, scroll_up=True, restore=False):
        model = self.view.get_model()

        self.__filter = None
        query = self.__search.get_query(star=["~people", "album"])
        if not query.matches_all:
            self.__filter = query.search
        self.__bg_filter = background_filter()

        self.__inhibit()

        # We could be smart and try to scroll to a selected album
        # but that introduces lots of wild scrolling. Feel free to change it.
        # Without scrolling the TV tries to stay at the same position
        # (40% down) which makes no sense, so always go to the top.
        if scroll_up:
            self.view.scroll_to_point(0, 0)

        # Don't filter on restore if there is nothing to filter
        if not restore or self.__filter or self.__bg_filter:
            model.refilter()

        self.__uninhibit()

    def __parse_query(self, model, iter_, data):
        f, b = self.__filter, self.__bg_filter

        if f is None and b is None:
            return True
        else:
            album = model.get_album(iter_)
            if album is None:
                return True
            elif b is None:
                return f(album)
            elif f is None:
                return b(album)
            else:
                return b(album) and f(album)

    def __search_func(self, model, column, key, iter_, data):
        album = model.get_album(iter_)
        if album is None:
            return True
        key = key.lower()
        title = album.title.lower()
        if key in title:
            return False
        if config.getboolean("browsers", "album_substrings"):
            people = (p.lower() for p in album.list("~people"))
            for person in people:
                if key in person:
                    return False
        return True

    def __popup(self, view, library):
        albums = self.__get_selected_albums()
        songs = self.__get_songs_from_albums(albums)

        items = []
        if self.__cover_column.get_visible():
            num = len(albums)
            button = MenuItem(
                ngettext("Reload album _cover", "Reload album _covers", num),
                Icons.VIEW_REFRESH)
            button.connect('activate', self.__refresh_album, view)
            items.append(button)

        menu = SongsMenu(library, songs, items=[items])
        menu.show_all()
        return view.popup_menu(menu, 0, Gtk.get_current_event_time())

    def __refresh_album(self, menuitem, view):
        items = self.__get_selected_items()
        for item in items:
            item.scanned = False
        model = self.view.get_model()
        for iter_, item in model.iterrows():
            if item in items:
                model.row_changed(model.get_path(iter_), iter_)

    def __get_selected_items(self):
        selection = self.view.get_selection()
        model, paths = selection.get_selected_rows()
        return model.get_items(paths)

    def __get_selected_albums(self):
        selection = self.view.get_selection()
        model, paths = selection.get_selected_rows()
        return model.get_albums(paths)

    def __get_songs_from_albums(self, albums, sort=True):
        # Sort first by how the albums appear in the model itself,
        # then within the album using the default order.
        songs = []
        if sort:
            for album in albums:
                songs.extend(sorted(album.songs, key=lambda s: s.sort_key))
        else:
            for album in albums:
                songs.extend(album.songs)
        return songs

    def __get_selected_songs(self, sort=True):
        albums = self.__get_selected_albums()
        return self.__get_songs_from_albums(albums, sort)

    def __drag_data_get(self, view, ctx, sel, tid, etime):
        songs = self.__get_selected_songs()
        if tid == 1:
            qltk.selection_set_songs(sel, songs)
        else:
            sel.set_uris([song("~uri") for song in songs])

    def __play_selection(self, view, indices, col):
        self.songs_activated()

    def active_filter(self, song):
        for album in self.__get_selected_albums():
            if song in album.songs:
                return True
        return False

    def can_filter_text(self):
        return True

    def filter_text(self, text):
        self.__search.set_text(text)
        if Query(text).is_parsable:
            self.__update_filter(self.__search, text)
            self.__inhibit()
            self.view.set_cursor((0, ))
            self.__uninhibit()
            self.activate()

    def get_filter_text(self):
        return self.__search.get_text()

    def can_filter(self, key):
        # Numerics are different for collections, and although title works,
        # it's not of much use here.
        if key is not None and (key.startswith("~#") or key == "title"):
            return False
        return super(AlbumList, self).can_filter(key)

    def can_filter_albums(self):
        return True

    def list_albums(self):
        model = self.view.get_model()
        return [row[0].album.key for row in model if row[0].album]

    def filter_albums(self, values):
        view = self.view
        self.__inhibit()
        changed = view.select_by_func(
            lambda r: r[0].album and r[0].album.key in values)
        self.view.grab_focus()
        self.__uninhibit()
        if changed:
            self.activate()

    def unfilter(self):
        self.filter_text("")
        self.view.set_cursor((0, ))

    def activate(self):
        self.view.get_selection().emit('changed')

    def __inhibit(self):
        self.view.handler_block(self.__sig)

    def __uninhibit(self):
        self.view.handler_unblock(self.__sig)

    def restore(self):
        text = config.gettext("browsers", "query_text")
        entry = self.__search
        entry.set_text(text)

        # update_filter expects a parsable query
        if Query(text).is_parsable:
            self.__update_filter(entry, text, scroll_up=False, restore=True)

        keys = config.gettext("browsers", "albums").split("\n")

        # FIXME: If albums is "" then it could be either all albums or
        # no albums. If it's "" and some other stuff, assume no albums,
        # otherwise all albums.
        self.__inhibit()
        if keys == [""]:
            self.view.set_cursor((0, ))
        else:

            def select_fun(row):
                album = row[0].album
                if not album:  # all
                    return False
                return album.str_key in keys

            self.view.select_by_func(select_fun)
        self.__uninhibit()

    def scroll(self, song):
        album_key = song.album_key
        select = lambda r: r[0].album and r[0].album.key == album_key
        self.view.select_by_func(select, one=True)

    def __get_config_string(self):
        selection = self.view.get_selection()
        model, paths = selection.get_selected_rows()

        # All is selected
        if model.contains_all(paths):
            return ""

        # All selected albums
        albums = model.get_albums(paths)

        confval = "\n".join((a.str_key for a in albums))
        # ConfigParser strips a trailing \n so we move it to the front
        if confval and confval[-1] == "\n":
            confval = "\n" + confval[:-1]
        return confval

    def save(self):
        conf = self.__get_config_string()
        config.settext("browsers", "albums", conf)
        text = self.__search.get_text()
        config.settext("browsers", "query_text", text)

    def __update_songs(self, view, selection):
        songs = self.__get_selected_songs(sort=False)
        self.songs_selected(songs)
Esempio n. 40
0
def main(argv=None):
    if argv is None:
        argv = sys_argv

    import quodlibet

    config_file = os.path.join(quodlibet.get_user_dir(), "config")
    quodlibet.init_cli(config_file=config_file)

    try:
        # we want basic commands not to import gtk (doubles process time)
        assert "gi.repository.Gtk" not in sys.modules
        sys.modules["gi.repository.Gtk"] = None
        startup_actions, cmds_todo = process_arguments(argv)
    finally:
        sys.modules.pop("gi.repository.Gtk", None)

    quodlibet.init()

    from quodlibet import app
    from quodlibet.qltk import add_signal_watch, Icons
    add_signal_watch(app.quit)

    import quodlibet.player
    import quodlibet.library
    from quodlibet import config
    from quodlibet import browsers
    from quodlibet import util

    app.name = "Quod Libet"
    app.id = "quodlibet"
    quodlibet.set_application_info(Icons.QUODLIBET, app.id, app.name)

    library_path = os.path.join(quodlibet.get_user_dir(), "songs")

    print_d("Initializing main library (%s)" % (
            quodlibet.util.path.unexpand(library_path)))

    library = quodlibet.library.init(library_path)
    app.library = library

    # this assumes that nullbe will always succeed
    from quodlibet.player import PlayerError
    wanted_backend = environ.get(
        "QUODLIBET_BACKEND", config.get("player", "backend"))

    try:
        player = quodlibet.player.init_player(wanted_backend, app.librarian)
    except PlayerError:
        print_exc()
        player = quodlibet.player.init_player("nullbe", app.librarian)

    app.player = player

    environ["PULSE_PROP_media.role"] = "music"
    environ["PULSE_PROP_application.icon_name"] = "quodlibet"

    browsers.init()

    from quodlibet.qltk.songlist import SongList, get_columns

    headers = get_columns()
    SongList.set_all_column_headers(headers)

    for opt in config.options("header_maps"):
        val = config.get("header_maps", opt)
        util.tags.add(opt, val)

    in_all = ("~filename ~uri ~#lastplayed ~#rating ~#playcount ~#skipcount "
              "~#added ~#bitrate ~current ~#laststarted ~basename "
              "~dirname").split()
    for Kind in browsers.browsers:
        if Kind.headers is not None:
            Kind.headers.extend(in_all)
        Kind.init(library)

    pm = quodlibet.init_plugins("no-plugins" in startup_actions)

    if hasattr(player, "init_plugins"):
        player.init_plugins()

    from quodlibet.qltk import unity
    unity.init("quodlibet.desktop", player)

    from quodlibet.qltk.songsmenu import SongsMenu
    SongsMenu.init_plugins()

    from quodlibet.util.cover import CoverManager
    app.cover_manager = CoverManager()
    app.cover_manager.init_plugins()

    from quodlibet.plugins.playlist import PLAYLIST_HANDLER
    PLAYLIST_HANDLER.init_plugins()

    from quodlibet.plugins.query import QUERY_HANDLER
    QUERY_HANDLER.init_plugins()

    from gi.repository import GLib

    def exec_commands(*args):
        for cmd in cmds_todo:
            try:
                resp = cmd_registry.run(app, *cmd)
            except CommandError:
                pass
            else:
                if resp is not None:
                    print_(resp, end="", flush=True)

    from quodlibet.qltk.quodlibetwindow import QuodLibetWindow, PlayerOptions
    # Call exec_commands after the window is restored, but make sure
    # it's after the mainloop has started so everything is set up.

    app.window = window = QuodLibetWindow(
        library, player,
        restore_cb=lambda:
            GLib.idle_add(exec_commands, priority=GLib.PRIORITY_HIGH))

    app.player_options = PlayerOptions(window)

    from quodlibet.qltk.window import Window

    from quodlibet.plugins.events import EventPluginHandler
    from quodlibet.plugins.gui import UserInterfacePluginHandler
    pm.register_handler(EventPluginHandler(library.librarian, player,
                                           app.window.songlist))
    pm.register_handler(UserInterfacePluginHandler())

    from quodlibet.mmkeys import MMKeysHandler
    from quodlibet.remote import Remote, RemoteError
    from quodlibet.commands import registry as cmd_registry, CommandError
    from quodlibet.qltk.tracker import SongTracker, FSInterface
    try:
        from quodlibet.qltk.dbus_ import DBusHandler
    except ImportError:
        DBusHandler = lambda player, library: None

    mmkeys_handler = MMKeysHandler(app)
    mmkeys_handler.start()

    current_path = os.path.join(quodlibet.get_user_dir(), "current")
    fsiface = FSInterface(current_path, player)
    remote = Remote(app, cmd_registry)
    try:
        remote.start()
    except RemoteError:
        exit_(1, True)

    DBusHandler(player, library)
    tracker = SongTracker(library.librarian, player, window.playlist)

    from quodlibet.qltk import session
    session.init("quodlibet")

    quodlibet.enable_periodic_save(save_library=True)

    if "start-playing" in startup_actions:
        player.paused = False

    if "start-hidden" in startup_actions:
        Window.prevent_inital_show(True)

    # restore browser windows
    from quodlibet.qltk.browser import LibraryBrowser
    GLib.idle_add(LibraryBrowser.restore, library, player,
                  priority=GLib.PRIORITY_HIGH)

    def before_quit():
        print_d("Saving active browser state")
        try:
            app.browser.save()
        except NotImplementedError:
            pass

        print_d("Shutting down player device %r." % player.version_info)
        player.destroy()

    quodlibet.run(window, before_quit=before_quit)

    app.player_options.destroy()
    quodlibet.finish_first_session(app.id)
    mmkeys_handler.quit()
    remote.stop()
    fsiface.destroy()

    tracker.destroy()
    quodlibet.library.save()

    config.save()

    print_d("Finished shutdown.")
Esempio n. 41
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super().__init__(dialog=False)

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(600, 480)

        main_box = Gtk.VBox()
        self.add(main_box)
        self.side_book = qltk.Notebook()

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after('drag-data-received',
                                    self.__songlist_drag_data_recv)
        self.song_scroller = ScrolledWindow()
        self.song_scroller.set_policy(Gtk.PolicyType.AUTOMATIC,
                                      Gtk.PolicyType.AUTOMATIC)
        self.song_scroller.set_shadow_type(Gtk.ShadowType.IN)
        self.song_scroller.add(self.songlist)

        self.qexpander = QueueExpander(library, player)
        self.qexpander.set_no_show_all(True)
        self.qexpander.set_visible(config.getboolean("memory", "queue"))

        def on_queue_visible(qex, param):
            config.set("memory", "queue", str(qex.get_visible()))

        self.qexpander.connect("notify::visible", on_queue_visible)

        self.playlist = PlaylistMux(player, self.qexpander.model,
                                    self.songlist.model)

        self.__player = player
        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, None, True)

        keyval, mod = Gtk.accelerator_parse("<Primary><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        self.__paned = paned = ConfigRHPaned("memory", "sidebar_pos", 0.25)
        paned.pack1(self.__browserbox, resize=True)
        # We'll pack2 when necessary (when the first sidebar plugin is set up)

        main_box.pack_start(paned, True, True, 0)

        play_order = PlayOrderWidget(self.songlist.model, player)
        statusbox = StatusBarBox(play_order, self.qexpander)
        self.order = play_order
        self.statusbar = statusbox.statusbar

        main_box.pack_start(Align(statusbox, border=3, top=-3), False, True, 0)

        self.songpane = SongListPaned(self.song_scroller, self.qexpander)
        self.songpane.show_all()

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self._select_browser(self, config.get("memory", "browser"),
                                 library, player, restore_browser)
        except:
            config.set("memory", "browser", browsers.name(browsers.default))
            config.save()
            raise

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_totals)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Esempio n. 42
0
from quodlibet.plugins import PluginManager
from quodlibet.pattern import FileFromPattern
from quodlibet.qltk._editutils import FilterPluginBox, FilterCheckButton
from quodlibet.qltk._editutils import EditingPluginHandler
from quodlibet.qltk.views import TreeViewColumn
from quodlibet.qltk.cbes import ComboBoxEntrySave
from quodlibet.qltk.models import ObjectStore
from quodlibet.qltk import Icons, Button
from quodlibet.qltk.wlw import WritingWindow
from quodlibet.util import connect_obj, gdecode
from quodlibet.util.path import fsdecode, fsnative
from quodlibet.util.path import strip_win32_incompat_from_path


NBP = os.path.join(quodlibet.get_user_dir(), "lists", "renamepatterns")
NBP_EXAMPLES = """\
<tracknumber>. <title>
<tracknumber|<tracknumber>. ><title>
<tracknumber> - <title>
<tracknumber> - <artist> - <title>
/path/<artist> - <album>/<tracknumber>. <title>
/path/<artist>/<album>/<tracknumber> - <title>"""


class SpacesToUnderscores(FilterCheckButton):
    _label = _("Replace spaces with _underscores")
    _section = "rename"
    _key = "spaces"
    _order = 1.0
Esempio n. 43
0
import os
from gi.repository import Gtk
import quodlibet
from quodlibet import formats, qltk
from quodlibet.qltk.wlw import WaitLoadWindow
from quodlibet.qltk.getstring import GetStringDialog
from quodlibet.util import escape
from quodlibet.util.collection import Playlist
from quodlibet.util.path import mkdir, fsdecode, is_fsnative


# Directory for playlist files
from quodlibet.util.uri import URI

PLAYLISTS = os.path.join(quodlibet.get_user_dir(), "playlists")
assert is_fsnative(PLAYLISTS)
if not os.path.isdir(PLAYLISTS):
    mkdir(PLAYLISTS)


class ConfirmRemovePlaylistDialog(qltk.Message):
    def __init__(self, parent, playlist):
        title = _("Are you sure you want to delete the playlist '%s'?") % escape(playlist.name)
        description = _("All information about the selected playlist " "will be deleted and can not be restored.")

        super(ConfirmRemovePlaylistDialog, self).__init__(
            Gtk.MessageType.WARNING, parent, title, description, Gtk.ButtonsType.NONE
        )

        self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_DELETE, Gtk.ResponseType.YES)
Esempio n. 44
0
 def load(self):
     with open(os.path.join(get_user_dir(), "feeds"), "rb") as f:
         self.__feeds = pickle.load(f)
Esempio n. 45
0
 def test_dirs(self):
     self.assertTrue(is_fsnative(quodlibet.get_base_dir()))
     self.assertTrue(is_fsnative(quodlibet.get_image_dir()))
     self.assertTrue(is_fsnative(quodlibet.get_user_dir()))
Esempio n. 46
0
 def write(self):
     with open(os.path.join(get_user_dir(), "feeds"), "wb") as f:
         pickle.dump(self.__feeds, f)
Esempio n. 47
0
from quodlibet import app

from quodlibet.browsers import Browser
from quodlibet.compat import listfilter, text_type, build_opener, PY2
from quodlibet.formats import AudioFile
from quodlibet.formats.remote import RemoteFile
from quodlibet.qltk.getstring import GetStringDialog
from quodlibet.qltk.msg import ErrorMessage
from quodlibet.qltk.views import AllTreeView
from quodlibet.qltk import Icons
from quodlibet.util import connect_obj, print_w
from quodlibet.qltk.x import ScrolledWindow, Align, Button, MenuItem
from quodlibet.util.picklehelper import pickle_load, pickle_dump, PickleError


FEEDS = os.path.join(quodlibet.get_user_dir(), "feeds")
DND_URI_LIST, DND_MOZ_URL = range(2)

# Migration path for pickle
sys.modules["browsers.audiofeeds"] = sys.modules[__name__]


class InvalidFeed(ValueError):
    pass


class Feed(list):
    def __init__(self, uri):
        self.name = _("Unknown")
        self.uri = uri
        self.changed = False
Esempio n. 48
0
class WebsiteSearch(SongsMenuPlugin):
    """Loads a browser with a URL designed to search on tags of the song.
    This may include a standard web search engine, eg Google, or a more
    specific site look-up. The URLs are customisable using tag patterns.
    """

    PLUGIN_ICON = Icons.APPLICATION_INTERNET
    PLUGIN_ID = "Website Search"
    PLUGIN_NAME = _("Website Search")
    PLUGIN_DESC = _("Searches your choice of website using any song tags.\n"
                    "Supports patterns e.g. %(pattern-example)s.") % {
                        "pattern-example":
                        "https://google.com?q=&lt;~artist~title&gt;"
                    }

    # Here are some starters...
    DEFAULT_URL_PATS = [
        ("Google song search", "https://google.com/search?q=<artist~title>"),
        ("Wikipedia (en) artist entry",
         "https://wikipedia.org/wiki/<albumartist|<albumartist>|<artist>>"),
        ("Musicbrainz album listing",
         "https://musicbrainz.org/<musicbrainz_albumid|release/"
         "<musicbrainz_albumid>|search?query=<album>&type=release>"),
        ("Discogs album search",
         "https://www.discogs.com/search?type=release&artist="
         "<albumartist|<albumartist>|<artist>>&title=<album>"),
        ("Youtube video search",
         "https://www.youtube.com/results?search_query=<artist~title>"),
        ("Go to ~website", "<website>"),
    ]
    PATTERNS_FILE = os.path.join(quodlibet.get_user_dir(), 'lists',
                                 'searchsites')

    def __set_site(self, name):
        self.chosen_site = name

    def get_url_pattern(self, key):
        """Gets the pattern for a given key"""
        return dict(self._url_pats).get(key, self.DEFAULT_URL_PATS[0][1])

    @classmethod
    def edit_patterns(cls, button):
        def valid_uri(s):
            # TODO: some pattern validation too (that isn't slow)
            try:
                p = Pattern(s)
                return (p and uri_is_valid(s))
            except ValueError:
                return False

        win = StandaloneEditor(filename=cls.PATTERNS_FILE,
                               title=_("Search URL patterns"),
                               initial=cls.DEFAULT_URL_PATS,
                               validator=valid_uri)
        win.show()

    @classmethod
    def PluginPreferences(cls, parent):
        hb = Gtk.HBox(spacing=3)
        hb.set_border_width(0)

        button = qltk.Button(_("Edit search URLs"), Icons.EDIT)
        button.connect("clicked", cls.edit_patterns)
        hb.pack_start(button, True, True, 0)
        hb.show_all()
        return hb

    def _get_saved_searches(self):
        filename = self.PATTERNS_FILE + ".saved"
        self._url_pats = StandaloneEditor.load_values(filename)
        # Failing all else...
        if not len(self._url_pats):
            print_d("No saved searches found in %s. Using defaults." %
                    filename)
            self._url_pats = self.DEFAULT_URL_PATS

    def __init__(self, *args, **kwargs):
        super(WebsiteSearch, self).__init__(*args, **kwargs)
        self.chosen_site = None
        self._url_pats = []
        submenu = Gtk.Menu()
        self._get_saved_searches()
        for name, url_pat in self._url_pats:
            item = Gtk.MenuItem(label=name)
            connect_obj(item, 'activate', self.__set_site, name)
            submenu.append(item)
        # Add link to editor
        configure = Gtk.MenuItem(label=_(u"Configure Searches…"))
        connect_obj(configure, 'activate', self.edit_patterns, configure)
        submenu.append(SeparatorMenuItem())
        submenu.append(configure)
        if submenu.get_children():
            self.set_submenu(submenu)
        else:
            self.set_sensitive(False)

    def plugin_songs(self, songs, launch=True) -> bool:
        # Check this is a launch, not a configure
        if self.chosen_site:
            url_pat = self.get_url_pattern(self.chosen_site)
            pat = Pattern(url_pat)
            # Remove Nones, and de-duplicate collection
            urls = set(filter(None, (website_for(pat, s) for s in songs)))
            if not urls:
                print_w("Couldn't build URLs using \"%s\"."
                        "Check your pattern?" % url_pat)
                return False
            print_d("Got %d websites from %d songs" % (len(urls), len(songs)))
            if launch:
                for url in urls:
                    website(url)
        return True
Esempio n. 49
0
#
# This program 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 2 of the License, or
# (at your option) any later version.

import os
import io

import quodlibet
from quodlibet import _
from quodlibet import util
from quodlibet.qltk import Icons
from quodlibet.plugins.events import EventPlugin

outfile = os.path.join(quodlibet.get_user_dir(), "jabber")
format = """\
<tune xmlns='http://jabber.org/protocol/tune'>
 <artist>%s</artist>
 <title>%s</title>
 <source>%s</source>
 <track>%d</track>
 <length>%d</length>
</tune>"""


class JEP118(EventPlugin):
    PLUGIN_ID = "JEP-118"
    PLUGIN_NAME = _("JEP-118")
    PLUGIN_DESC = _("Outputs a Jabber User Tunes file to ~/.quodlibet/jabber.")
    PLUGIN_ICON = Icons.DOCUMENT_SAVE
Esempio n. 50
0
def get_path():
    out = os.path.join(quodlibet.get_user_dir(), "current.cover")
    return config.get("plugins", __name__, out)
Esempio n. 51
0
from quodlibet import util
from quodlibet import app

from quodlibet.browsers import Browser
from quodlibet.formats import AudioFile
from quodlibet.formats.remote import RemoteFile
from quodlibet.qltk.getstring import GetStringDialog
from quodlibet.qltk.msg import ErrorMessage
from quodlibet.qltk.views import AllTreeView
from quodlibet.qltk import Icons
from quodlibet.util import connect_obj, print_w
from quodlibet.qltk.x import ScrolledWindow, Align, Button, MenuItem
from quodlibet.util.picklehelper import pickle_load, pickle_dump, PickleError


FEEDS = os.path.join(quodlibet.get_user_dir(), "feeds")
DND_URI_LIST, DND_MOZ_URL = range(2)

# Migration path for pickle
sys.modules["browsers.audiofeeds"] = sys.modules[__name__]


class InvalidFeed(ValueError):
    pass


class Feed(list):
    def __init__(self, uri):
        self.name = _("Unknown")
        self.uri = uri
        self.changed = False
Esempio n. 52
0
from quodlibet.qltk.views import AllTreeView
from quodlibet.qltk.x import MenuItem, Align, ScrolledWindow, RadioMenuItem
from quodlibet.qltk.x import SymbolicIconImage
from quodlibet.qltk.searchbar import SearchBarBox
from quodlibet.qltk.menubutton import MenuButton
from quodlibet.qltk import Icons
from quodlibet.util import copool, connect_destroy
from quodlibet.util.library import background_filter
from quodlibet.util import connect_obj, DeferredSignal
from quodlibet.util.collection import Album
from quodlibet.qltk.cover import get_no_cover_pixbuf
from quodlibet.qltk.image import (get_pbosf_for_pixbuf, get_scale_factor,
    set_renderer_from_pbosf, add_border_widget)


PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "album_pattern")


class AlbumTagCompletion(EntryWordCompletion):
    def __init__(self):
        super(AlbumTagCompletion, self).__init__()
        try:
            model = self.__model
        except AttributeError:
            model = type(self).__model = Gtk.ListStore(str)
            self.__refreshmodel()
        self.set_model(model)
        self.set_text_column(0)

    def __refreshmodel(self, *args):
        for tag in ["title", "album", "date", "people", "artist", "genre"]:
Esempio n. 53
0
                               package=__package__,
                               load_compiled=load_pyc)

    for mod in modules:
        try:
            devices.extend(mod.devices)
        except AttributeError:
            print_w("%r doesn't contain any devices." % mod.__name__)

    devices.sort()


if not util.is_osx() and not util.is_windows():
    init_devices()

DEVICES = os.path.join(quodlibet.get_user_dir(), "devices")

config = ConfigParser.RawConfigParser()
config.read(DEVICES)


def write():
    f = file(DEVICES, 'w')
    config.write(f)
    f.close()


# Return a constructor for a device given by a string
def get(name):
    try:
        return devices[[d.__name__ for d in devices].index(name)]
Esempio n. 54
0
 def test_dirs(self):
     self.assertTrue(isinstance(quodlibet.get_base_dir(), fsnative))
     self.assertTrue(isinstance(quodlibet.get_image_dir(), fsnative))
     self.assertTrue(isinstance(quodlibet.get_user_dir(), fsnative))
     self.assertTrue(isinstance(quodlibet.get_cache_dir(), fsnative))
Esempio n. 55
0
from quodlibet.formats import AudioFileError
from quodlibet.plugins import PluginManager
from quodlibet.qltk._editutils import FilterPluginBox, FilterCheckButton
from quodlibet.qltk._editutils import EditingPluginHandler, OverwriteWarning
from quodlibet.qltk._editutils import WriteFailedError
from quodlibet.qltk.wlw import WritingWindow
from quodlibet.qltk.views import TreeViewColumn
from quodlibet.qltk.cbes import ComboBoxEntrySave
from quodlibet.qltk.models import ObjectStore
from quodlibet.qltk import Icons
from quodlibet.util.tagsfrompath import TagsFromPattern
from quodlibet.util.string.splitters import split_value
from quodlibet.util import connect_obj, gdecode
from quodlibet.compat import itervalues

TBP = os.path.join(quodlibet.get_user_dir(), "lists", "tagpatterns")
TBP_EXAMPLES = """\
<tracknumber>. <title>
<tracknumber> - <title>
<tracknumber> - <artist> - <title>
<artist> - <album>/<tracknumber>. <title>
<artist>/<album>/<tracknumber> - <title>"""


class UnderscoresToSpaces(FilterCheckButton):
    _label = _("Replace _underscores with spaces")
    _section = "tagsfrompath"
    _key = "underscores"
    _order = 1.0

    def filter(self, tag, value):
Esempio n. 56
0
class QLSubmitQueue(object):
    """Manages the submit queue for scrobbles. Works independently of the
    QLScrobbler plugin being enabled; other plugins may use submit() to queue
    songs for scrobbling.
    """

    CLIENT = "qlb"
    CLIENT_VERSION = const.VERSION
    PROTOCOL_VERSION = "1.2"
    DUMP = os.path.join(quodlibet.get_user_dir(), "scrobbler_cache_v2")

    # These objects are shared across instances, to allow other plugins to
    # queue scrobbles in future versions of QL
    queue = []
    changed_event = threading.Event()

    def set_nowplaying(self, song):
        """Send a Now Playing notification."""
        formatted = self._format_song(song)
        if not formatted or self.nowplaying_song == formatted:
            return
        self.nowplaying_song = formatted
        self.nowplaying_sent = False
        self.changed()

    def submit(self, song, timestamp=0):
        """Submit a song. If 'timestamp' is 0, the current time will
        be used."""
        formatted = self._format_song(song)
        if formatted is None:
            return
        if timestamp > 0:
            formatted['i'] = str(timestamp)
        elif timestamp == 0:
            formatted['i'] = str(int(time.time()))
        else:
            # TODO: Forging timestamps for submission from PMPs
            return
        self.queue.append(formatted)
        self.changed()

    def _format_song(self, song):
        """Returns a dict with the keys formatted as required by spec."""
        store = {
            "l": str(song.get("~#length", 0)),
            "n": str(song("~#track")),
            "b": song.comma("album"),
            "m": song("musicbrainz_trackid"),
            "t": self.titlepat.format(song),
            "a": self.artpat.format(song),
        }

        # Spec requires title and artist at minimum
        if not (store.get("a") and store.get("t")):
            return None
        return store

    def __init__(self):
        self.nowplaying_song = None
        self.nowplaying_sent = False
        self.sessionid = None

        self.broken = False

        self.username, self.password, self.base_url = ('', '', '')

        # These need to be set early for _format_song to work
        self.titlepat = Pattern(config_get_title_pattern())
        self.artpat = Pattern(config_get_artist_pattern())

        try:
            with open(self.DUMP, 'rb') as disk_queue_file:
                disk_queue = pickle_load(disk_queue_file)
            os.unlink(self.DUMP)
            self.queue += disk_queue
        except (EnvironmentError, PickleError):
            pass

    @classmethod
    def dump_queue(klass):
        if klass.queue:
            try:
                with open(klass.DUMP, 'wb') as disk_queue_file:
                    pickle_dump(klass.queue, disk_queue_file)
            except (EnvironmentError, PickleError):
                pass

    def _check_config(self):
        user = plugin_config.get('username')
        passw = md5(plugin_config.getbytes('password')).hexdigest()
        url = config_get_url()
        if not user or not passw or not url:
            if self.queue and not self.broken:
                self.quick_dialog(
                    _("Please visit the Plugins window to set "
                      "QLScrobbler up. Until then, songs will not be "
                      "submitted."), Gtk.MessageType.INFO)
                self.broken = True
        elif (self.username, self.password, self.base_url) != (user, passw,
                                                               url):
            self.username, self.password, self.base_url = (user, passw, url)
            self.broken = False
            self.handshake_sent = False
        self.offline = plugin_config.getboolean('offline')
        self.titlepat = Pattern(config_get_title_pattern())
        self.artpat = Pattern(config_get_artist_pattern())

    def changed(self):
        """Signal that settings or queue contents were changed."""
        self._check_config()
        if not self.broken and not self.offline and (
                self.queue or
            (self.nowplaying_song and not self.nowplaying_sent)):
            self.changed_event.set()
            return
        self.changed_event.clear()

    def run(self):
        """Submit songs from the queue. Call from a daemon thread."""
        # The spec calls for exponential backoff of failed handshakes, with a
        # minimum of 1m and maximum of 120m delay between attempts.
        self.handshake_sent = False
        self.handshake_event = threading.Event()
        self.handshake_event.set()
        self.handshake_delay = 1

        self.failures = 0

        while True:
            self.changed_event.wait()
            if not self.handshake_sent:
                self.handshake_event.wait()
                if self.send_handshake():
                    self.failures = 0
                    self.handshake_delay = 1
                    self.handshake_sent = True
                else:
                    self.handshake_event.clear()
                    self.handshake_delay = min(self.handshake_delay * 2, 120)
                    GLib.timeout_add(self.handshake_delay * 60 * 1000,
                                     self.handshake_event.set)
                    continue
            self.changed_event.wait()
            if self.queue:
                if self.send_submission():
                    self.failures = 0
                else:
                    self.failures += 1
                    if self.failures >= 3:
                        self.handshake_sent = False
            elif self.nowplaying_song and not self.nowplaying_sent:
                self.send_nowplaying()
                self.nowplaying_sent = True
            else:
                # Nothing left to do; wait until something changes
                self.changed_event.clear()

    def send_handshake(self, show_dialog=False):
        # construct url
        stamp = int(time.time())
        auth = md5(self.password.encode("utf-8") +
                   str(stamp).encode("utf-8")).hexdigest()
        url = "%s/?hs=true&p=%s&c=%s&v=%s&u=%s&a=%s&t=%d" % (
            self.base_url, self.PROTOCOL_VERSION, self.CLIENT,
            self.CLIENT_VERSION, self.username, auth, stamp)
        print_d("Sending handshake to service.")

        try:
            resp = urlopen(url)
        except UrllibError:
            if show_dialog:
                self.quick_dialog(
                    _("Could not contact service '%s'.") %
                    util.escape(self.base_url), Gtk.MessageType.ERROR)
            else:
                print_d("Could not contact service. Queueing submissions.")
            return False
        except ValueError:
            self.quick_dialog(_("Authentication failed: invalid URL."),
                              Gtk.MessageType.ERROR)
            self.broken = True
            return False

        # check response
        lines = resp.read().decode("utf-8", "ignore").rstrip().split("\n")
        status = lines.pop(0)
        print_d("Handshake status: %s" % status)

        if status == "OK":
            self.session_id, self.nowplaying_url, self.submit_url = lines
            self.handshake_sent = True
            print_d("Session ID: %s, NP URL: %s, Submit URL: %s" %
                    (self.session_id, self.nowplaying_url, self.submit_url))
            return True
        elif status == "BADAUTH":
            self.quick_dialog(
                _("Authentication failed: Invalid username '%s' "
                  "or bad password.") % util.escape(self.username),
                Gtk.MessageType.ERROR)
            self.broken = True
        elif status == "BANNED":
            self.quick_dialog(_("Client is banned. Contact the author."),
                              Gtk.MessageType.ERROR)
            self.broken = True
        elif status == "BADTIME":
            self.quick_dialog(
                _("Wrong system time. Submissions may fail "
                  "until it is corrected."), Gtk.MessageType.ERROR)
        else:  # "FAILED"
            self.quick_dialog(util.escape(status), Gtk.MessageType.ERROR)
        self.changed()
        return False

    def _check_submit(self, url, data):
        data_str = urlencode(data).encode("ascii")
        try:
            resp = urlopen(url, data_str)
        except EnvironmentError:
            print_d("Audioscrobbler server not responding, will try later.")
            return False

        resp_save = resp.read().decode("utf-8", "ignore")
        status = resp_save.rstrip().split("\n")[0]
        print_d("Submission status: %s" % status)

        if status == "OK":
            return True
        elif status == "BADSESSION":
            self.handshake_sent = False
            return False
        else:
            return False

    def send_submission(self):
        data = {'s': self.session_id}
        to_submit = self.queue[:min(len(self.queue), 50)]
        for idx, song in enumerate(to_submit):
            for key, val in song.items():
                data['%s[%d]' % (key, idx)] = val.encode('utf-8')
            data['o[%d]' % idx] = 'P'
            data['r[%d]' % idx] = ''

        print_d('Submitting song(s): %s' %
                ('\n\t'.join(['%s - %s' % (s['a'], s['t'])
                              for s in to_submit])))

        if self._check_submit(self.submit_url, data):
            del self.queue[:len(to_submit)]
            return True
        else:
            return False

    def send_nowplaying(self):
        data = {'s': self.session_id}
        for key, val in self.nowplaying_song.items():
            data[key] = val.encode('utf-8')
        print_d('Now playing song: %s - %s' %
                (self.nowplaying_song['a'], self.nowplaying_song['t']))

        return self._check_submit(self.nowplaying_url, data)

    def quick_dialog_helper(self, dialog_type, msg):
        dialog = Message(dialog_type, app.window, "QLScrobbler", msg)
        dialog.connect('response', lambda dia, resp: dia.destroy())
        dialog.show()

    def quick_dialog(self, msg, dialog_type):
        GLib.idle_add(self.quick_dialog_helper, dialog_type, msg)
Esempio n. 57
0
from quodlibet.formats import AudioFileError
from quodlibet.plugins import PluginManager
from quodlibet.qltk._editutils import FilterPluginBox, FilterCheckButton
from quodlibet.qltk._editutils import EditingPluginHandler, OverwriteWarning
from quodlibet.qltk._editutils import WriteFailedError
from quodlibet.qltk.wlw import WritingWindow
from quodlibet.qltk.views import TreeViewColumn
from quodlibet.qltk.cbes import ComboBoxEntrySave
from quodlibet.qltk.models import ObjectStore
from quodlibet.qltk import Icons
from quodlibet.util.tagsfrompath import TagsFromPattern
from quodlibet.util.string.splitters import split_value
from quodlibet.util import connect_obj, gdecode


TBP = os.path.join(quodlibet.get_user_dir(), "lists", "tagpatterns")
TBP_EXAMPLES = """\
<tracknumber>. <title>
<tracknumber> - <title>
<tracknumber> - <artist> - <title>
<artist> - <album>/<tracknumber>. <title>
<artist>/<album>/<tracknumber> - <title>"""


class UnderscoresToSpaces(FilterCheckButton):
    _label = _("Replace _underscores with spaces")
    _section = "tagsfrompath"
    _key = "underscores"
    _order = 1.0

    def filter(self, tag, value):