Example #1
0
class ExactRating(SongsMenuPlugin):
    PLUGIN_ID = "exact-rating"
    PLUGIN_NAME = _("Set Exact Rating")
    PLUGIN_DESC = _("Allows setting the rating of songs with a number.")
    REQUIRES_ACTION = True
    PLUGIN_ICON = Icons.USER_BOOKMARKS

    plugin_handles = any_song(lambda s: s.can_change())

    def plugin_songs(self, songs):
        value = -1
        while not 0 <= value <= 1:
            input_string = GetStringDialog(
                self.plugin_window, self.PLUGIN_NAME,
                _("Please give your desired rating on a scale "
                  "from 0.0 to 1.0"), _("_Apply"), Icons.NONE).run()

            if input_string is None:
                return

            try:
                value = float(input_string)
            except ValueError:
                continue

        count = len(songs)
        if (count > 1
                and config.getboolean("browsers", "rating_confirm_multiple")):
            confirm_dialog = ConfirmRateMultipleDialog(self.plugin_window,
                                                       count, value)
            if confirm_dialog.run() != Gtk.ResponseType.YES:
                return

        for song in songs:
            song["~#rating"] = value
Example #2
0
class RemoveID3TLEN(SongsMenuPlugin):
    PLUGIN_ID = "RemoveID3TLEN"
    PLUGIN_NAME = _("Fix MP3 Duration")
    PLUGIN_DESC = _("Removes TLEN frames from ID3 tags which can be the cause "
                    "for invalid song durations.")
    PLUGIN_ICON = Icons.EDIT_CLEAR

    plugin_handles = any_song(is_an_id3, is_writable)

    def plugin_songs(self, songs):
        for song in songs:
            song = song._song
            if not isinstance(song, ID3File):
                continue

            filename = song["~filename"]

            try:
                tag = ID3(filename)
            except Exception:
                util.print_exc()
                continue

            if not tag.getall("TLEN"):
                continue

            tag.delall("TLEN")

            try:
                tag.save()
            except Exception:
                util.print_exc()
                continue

            app.librarian.reload(song)
Example #3
0
class ForceWrite(SongsMenuPlugin):
    PLUGIN_ID = "Force Write"
    PLUGIN_NAME = _("Force Write")
    PLUGIN_DESC = _("Saves the files again. This will make sure play counts "
                    "and ratings are up to date.")
    PLUGIN_ICON = Icons.DOCUMENT_SAVE

    plugin_handles = any_song(is_writable)

    def plugin_song(self, song):
        song._needs_write = True
Example #4
0
class ForceWrite(SongsMenuPlugin):
    PLUGIN_ID = "Force Write"
    PLUGIN_NAME = _("Update Tags in Files")
    PLUGIN_DESC = _("Update modified tags in files. "
                    "This will ensure play counts and ratings are up to date.")
    PLUGIN_ICON = Icons.DOCUMENT_SAVE

    plugin_handles = any_song(is_writable)

    def plugin_song(self, song):
        song._needs_write = True
Example #5
0
class SplitAlbum(SongsMenuPlugin):
    PLUGIN_ID = "Split Album"
    PLUGIN_NAME = _("Split Album")
    PLUGIN_DESC = _("Split out disc number.")
    PLUGIN_ICON = Icons.EDIT_FIND_REPLACE

    plugin_handles = any_song(has_album_splittable)

    def plugin_song(self, song):
        if has_album_splittable(song):
            album, disc = split_album(song["album"])
            if album:
                song["album"] = album
            if disc:
                song["discnumber"] = disc
Example #6
0
class BrowseFolders(SongsMenuPlugin):
    PLUGIN_ID = 'Browse Folders'
    PLUGIN_NAME = _('Browse Folders')
    PLUGIN_DESC = _("Opens the songs' folders in a file manager.")
    PLUGIN_ICON = Icons.DOCUMENT_OPEN

    def plugin_songs(self, songs):
        songs = [s for s in songs if s.is_file]
        print_d("Trying to browse folders...")
        if not show_songs(songs):
            ErrorMessage(self.plugin_window,
                         _("Unable to open folders"),
                         _("No program available to open folders.")).run()

    plugin_handles = any_song(is_a_file)
    """By default, any single song being a file is good enough"""
Example #7
0
class DownloadCoverArt(SongsMenuPlugin):
    """Download and save album (cover) art from a variety of sources"""

    PLUGIN_ID = 'Download Cover Art'
    PLUGIN_NAME = _('Download Cover Art')
    PLUGIN_DESC = _('Downloads high-quality album covers using cover plugins.')
    PLUGIN_ICON = Icons.INSERT_IMAGE
    REQUIRES_ACTION = True

    plugin_handles = any_song(is_a_file)

    def plugin_album(self, songs):
        manager = app.cover_manager
        dialog = CoverArtWindow(songs, manager, Config())
        ret = dialog.run()
        if ret == Gtk.ResponseType.APPLY:
            manager.cover_changed(songs)
        dialog.destroy()
Example #8
0
class MakeSortTags(SongsMenuPlugin):
    PLUGIN_ID = "SortTags"
    PLUGIN_NAME = _("Create Sort Tags")
    PLUGIN_DESC = _("Converts album and artist names to sort names, poorly.")
    PLUGIN_ICON = Icons.EDIT

    plugin_handles = any_song(is_writable, is_finite)

    def plugin_song(self, song):
        for tag in ["album"]:
            values = filter(None, map(album_to_sort, song.list(tag)))
            if values and (tag + "sort") not in song:
                song[tag + "sort"] = "\n".join(values)

        for tag in ["artist", "albumartist", "performer"]:
            values = filter(None, map(artist_to_sort, song.list(tag)))
            if values and (tag + "sort") not in song:
                song[tag + "sort"] = "\n".join(values)
Example #9
0
class FilterBrowser(SongsMenuPlugin):
    PLUGIN_ID = 'filterbrowser'
    PLUGIN_NAME = _('Filter on Directory')
    PLUGIN_DESC = _("Filters on directory in a new browser window.")
    PLUGIN_ICON = Icons.EDIT_SELECT_ALL

    plugin_handles = any_song(is_a_file)

    def plugin_songs(self, songs):
        tag = "~dirname"

        values = []
        for song in songs:
            values.extend(song.list(tag))

        browser = LibraryBrowser.open(browsers.get("SearchBar"), app.library,
                                      app.player)
        browser.browser.filter(tag, set(values))
Example #10
0
class BrowseFolders(SongsMenuPlugin):
    PLUGIN_ID = 'Browse Folders'
    PLUGIN_NAME = _('Browse Folders')
    PLUGIN_DESC = _("Opens the songs' folders in a file manager.")
    PLUGIN_ICON = Icons.DOCUMENT_OPEN

    _HANDLERS = [
        browse_folders_fdo, browse_folders_thunar, browse_folders_xdg_open,
        browse_folders_gnome_open, browse_folders_win_explorer,
        browse_folders_finder
    ]

    def plugin_songs(self, songs):
        songs = [s for s in songs if s.is_file]
        print_d("Trying to browse folders...")
        if not self._handle(songs):
            ErrorMessage(self.plugin_window, _("Unable to open folders"),
                         _("No program available to open folders.")).run()

    plugin_handles = any_song(is_a_file)
    """By default, any single song being a file is good enough"""

    def _handle(self, songs):
        """
        Uses the first successful handler in callable list `_HANDLERS`
        to handle `songs`
        Returns False if none could be used
        """

        for handler in self._HANDLERS:
            name = handler.__name__
            try:
                print_d("Trying %r..." % name)
                handler(songs)
            except BrowseError as e:
                print_d("...failed: %r" % e)
            else:
                print_d("...success!")
                return True
        print_d("No handlers could be used.")
        return False
Example #11
0
class DownloadAlbumArt(SongsMenuPlugin, PluginConfigMixin):
    """Download and save album (cover) art from a variety of sources"""

    PLUGIN_ID = 'Download Album Art'
    PLUGIN_NAME = _('Download Album Art')
    PLUGIN_DESC = _('Downloads album covers from various websites.')
    PLUGIN_ICON = Icons.INSERT_IMAGE
    CONFIG_SECTION = PLUGIN_CONFIG_SECTION
    REQUIRES_ACTION = True

    plugin_handles = any_song(is_a_file)

    @classmethod
    def PluginPreferences(cls, window):
        table = Gtk.Table(n_rows=len(ENGINES), n_columns=2)
        table.props.expand = False
        table.set_col_spacings(6)
        table.set_row_spacings(6)
        frame = qltk.Frame(_("Sources"), child=table)

        for i, eng in enumerate(sorted(ENGINES, key=lambda x: x["url"])):
            check = cls.ConfigCheckButton(eng['config_id'].title(),
                                          CONFIG_ENG_PREFIX + eng['config_id'],
                                          True)
            table.attach(check, 0, 1, i, i + 1)

            button = Gtk.Button(label=eng['url'])
            button.connect('clicked', lambda s: util.website(s.get_label()))
            table.attach(button,
                         1,
                         2,
                         i,
                         i + 1,
                         xoptions=Gtk.AttachOptions.FILL
                         | Gtk.AttachOptions.SHRINK)
        return frame

    def plugin_album(self, songs):
        return AlbumArtWindow(songs)
Example #12
0
class SplitTags(SongsMenuPlugin):
    PLUGIN_ID = "Split Tags"
    PLUGIN_NAME = _("Split Tags")
    PLUGIN_DESC = _("Splits the disc number from the album and the version "
                    "from the title at the same time.")
    PLUGIN_ICON = Icons.EDIT_FIND_REPLACE

    plugin_handles = any_song(has_title_splittable)

    def plugin_song(self, song):
        if has_title_splittable(song):
            title, versions = split_title(song["title"])
            if title:
                song["title"] = title
            if versions:
                song["version"] = "\n".join(versions)

        if has_album_splittable(song):
            album, disc = split_album(song["album"])
            if album:
                song["album"] = album
            if disc:
                song["discnumber"] = disc
Example #13
0
class Duplicates(SongsMenuPlugin, PluginConfigMixin):
    PLUGIN_ID = 'Duplicates'
    PLUGIN_NAME = _('Duplicates Browser')
    PLUGIN_DESC = _('Finds and displays similarly tagged versions of songs.')
    PLUGIN_ICON = Icons.EDIT_SELECT_ALL

    MIN_GROUP_SIZE = 2
    _CFG_KEY_KEY = "key_expression"
    __DEFAULT_KEY_VALUE = "~artist~title~version"

    _CFG_REMOVE_WHITESPACE = 'remove_whitespace'
    _CFG_REMOVE_DIACRITICS = 'remove_diacritics'
    _CFG_REMOVE_PUNCTUATION = 'remove_punctuation'
    _CFG_CASE_INSENSITIVE = 'case_insensitive'

    plugin_handles = any_song(is_finite)

    # Cached values
    key_expression = None
    __cfg_cache = {}

    # Faster than a speeding bullet
    if PY2:
        __trans = "".join(map(chr, range(256)))
    else:
        __trans = str.maketrans({ord(k): None for k in string.punctuation})

    @classmethod
    def get_key_expression(cls):
        if not cls.key_expression:
            cls.key_expression = (
                    cls.config_get(cls._CFG_KEY_KEY, cls.__DEFAULT_KEY_VALUE))
        return cls.key_expression

    @classmethod
    def PluginPreferences(cls, window):
        def key_changed(entry):
            cls.key_expression = None
            cls.config_set(cls._CFG_KEY_KEY, entry.get_text().strip())

        vb = Gtk.VBox(spacing=10)
        vb.set_border_width(0)
        hbox = Gtk.HBox(spacing=6)
        # TODO: construct a decent validator and use ValidatingEntry
        e = UndoEntry()
        e.set_text(cls.get_key_expression())
        e.connect("changed", key_changed)
        e.set_tooltip_markup(_("Accepts QL tag expressions like "
                "<tt>~artist~title</tt> or <tt>musicbrainz_track_id</tt>"))
        lbl = Gtk.Label(label=_("_Group duplicates by:"))
        lbl.set_mnemonic_widget(e)
        lbl.set_use_underline(True)
        hbox.pack_start(lbl, False, True, 0)
        hbox.pack_start(e, True, True, 0)
        frame = qltk.Frame(label=_("Duplicate Key"), child=hbox)
        vb.pack_start(frame, True, True, 0)

        # Matching Option
        toggles = [
            (cls._CFG_REMOVE_WHITESPACE, _("Remove _Whitespace")),
            (cls._CFG_REMOVE_DIACRITICS, _("Remove _Diacritics")),
            (cls._CFG_REMOVE_PUNCTUATION, _("Remove _Punctuation")),
            (cls._CFG_CASE_INSENSITIVE, _("Case _Insensitive")),
        ]
        vb2 = Gtk.VBox(spacing=6)
        for key, label in toggles:
            ccb = ConfigCheckButton(label, 'plugins', cls._config_key(key))
            ccb.set_active(cls.config_get_bool(key))
            vb2.pack_start(ccb, True, True, 0)

        frame = qltk.Frame(label=_("Matching options"), child=vb2)
        vb.pack_start(frame, False, True, 0)

        vb.show_all()
        return vb

    @staticmethod
    def remove_accents(s):
        return "".join(c for c in unicodedata.normalize('NFKD', text_type(s))
                       if not unicodedata.combining(c))

    @classmethod
    def get_key(cls, song):
        key = str(song(cls.get_key_expression()))
        if cls.config_get_bool(cls._CFG_REMOVE_DIACRITICS):
            key = cls.remove_accents(key)
        if cls.config_get_bool(cls._CFG_CASE_INSENSITIVE):
            key = key.lower()
        if cls.config_get_bool(cls._CFG_REMOVE_PUNCTUATION):
            key = (key.translate(cls.__trans, string.punctuation) if PY2
                   else key.translate(cls.__trans))

        if cls.config_get_bool(cls._CFG_REMOVE_WHITESPACE):
            key = "_".join(key.split())
        return key

    def plugin_songs(self, songs):
        model = DuplicatesTreeModel()
        self.__cfg_cache = {}

        # Index all songs by our custom key
        # TODO: make this cache-friendly
        print_d("Calculating duplicates for %d song(s)..." % len(songs))
        groups = {}
        for song in songs:
            key = self.get_key(song)
            if key and key in groups:
                groups[key].add(song._song)
            elif key:
                groups[key] = {song._song}

        for song in app.library:
            key = self.get_key(song)
            if key in groups:
                groups[key].add(song)

        # Now display the grouped duplicates
        for (key, children) in groups.items():
            if len(children) < self.MIN_GROUP_SIZE:
                continue
            # The parent (group) label
            model.add_group(key, children)

        dialog = DuplicateDialog(model)
        dialog.show()
 def test_any_song(self):
     FakeSongsMenuPlugin.plugin_handles = any_song(even)
     p = FakeSongsMenuPlugin(self.songs, None)
     self.failUnless(p.plugin_handles(self.songs))
     self.failIf(p.plugin_handles(self.songs[:1]))
 def test_any_song_multiple(self):
     FakeSongsMenuPlugin.plugin_handles = any_song(even, never)
     p = FakeSongsMenuPlugin(self.songs, None)
     self.failIf(p.plugin_handles(self.songs))
     self.failIf(p.plugin_handles(self.songs[:1]))
Example #16
0
class Bookmarks(SongsMenuPlugin):
    PLUGIN_ID = "Go to Bookmark"
    PLUGIN_NAME = _(u"Go to Bookmark")
    PLUGIN_DESC = _("Manages bookmarks in the selected files.")
    PLUGIN_ICON = Icons.GO_JUMP

    plugin_handles = any_song(has_bookmark)

    def __init__(self, songs, *args, **kwargs):
        super(Bookmarks, self).__init__(songs, *args, **kwargs)
        self.__menu = Gtk.Menu()
        self.__menu.connect('map', self.__map, songs)
        self.__menu.connect('unmap', self.__unmap)
        self.set_submenu(self.__menu)

    class FakePlayer(object):
        def __init__(self, song):
            self.song = song

        def seek(self, time):
            if app.player.go_to(self.song._song, explicit=True):
                app.player.seek(time)

        get_position = lambda *x: 0

    def __map(self, menu, songs):
        for song in songs:
            marks = song.bookmarks
            if marks:
                fake_player = self.FakePlayer(song)

                song_item = Gtk.MenuItem(song.comma("title"))
                song_menu = Gtk.Menu()
                song_item.set_submenu(song_menu)
                menu.append(song_item)

                items = qltk.bookmarks.MenuItems(marks, fake_player, True)
                for item in items:
                    song_menu.append(item)

                song_menu.append(SeparatorMenuItem())
                i = qltk.MenuItem(_(u"_Edit Bookmarks…"), Icons.EDIT)

                def edit_bookmarks_cb(menu_item):
                    window = EditBookmarks(self.plugin_window, app.library,
                                           fake_player)
                    window.show()

                i.connect('activate', edit_bookmarks_cb)
                song_menu.append(i)

        if menu.get_active() is None:
            no_marks = Gtk.MenuItem(_("No Bookmarks"))
            no_marks.set_sensitive(False)
            menu.append(no_marks)

        menu.show_all()

    def __unmap(self, menu):
        for child in self.__menu.get_children():
            self.__menu.remove(child)

    def plugin_songs(self, songs):
        pass
Example #17
0
 def test_any_song(self):
     FakeSongsMenuPlugin.plugin_handles = any_song(even)
     p = FakeSongsMenuPlugin(self.songs, None)
     self.failUnless(p.plugin_handles(self.songs))
     self.failIf(p.plugin_handles(self.songs[:1]))
Example #18
0
class EditEmbedded(SongsMenuPlugin):
    PLUGIN_ID = "embedded_edit"
    PLUGIN_NAME = _("Edit Embedded Images")
    PLUGIN_DESC = _("Removes or replaces embedded images.")
    PLUGIN_ICON = Icons.INSERT_IMAGE

    plugin_handles = any_song(has_writable_image)
    """if any song supports editing, we are active"""
    def __init__(self, songs, *args, **kwargs):
        super(EditEmbedded, self).__init__(songs, *args, **kwargs)
        self.__menu = Gtk.Menu()
        self.__menu.connect('map', self.__map, songs)
        self.__menu.connect('unmap', self.__unmap)
        self.set_submenu(self.__menu)

    def __remove_images(self, menu_item, songs):
        win = WritingWindow(self.plugin_window, len(songs))
        win.show()

        for song in songs:
            if song.has_images and song.can_change_images:
                song.clear_images()

            if win.step():
                break

        win.destroy()
        self.plugin_finish()

    def __set_image(self, menu_item, songs):
        win = WritingWindow(self.plugin_window, len(songs))
        win.show()

        for song in songs:
            if song.can_change_images:
                fileobj = app.cover_manager.get_cover(song)
                if fileobj:
                    path = fileobj.name
                    image = EmbeddedImage.from_path(path)
                    if image:
                        song.set_image(image)

            if win.step():
                break

        win.destroy()
        self.plugin_finish()

    def __map(self, menu, songs):
        remove_item = MenuItem(_("_Remove all Images"), "edit-delete")
        remove_item.connect('activate', self.__remove_images, songs)
        menu.append(remove_item)

        set_item = MenuItem(_("_Embed Current Image"), "edit-paste")
        set_item.connect('activate', self.__set_image, songs)
        menu.append(set_item)

        menu.show_all()

    def __unmap(self, menu):
        for child in self.__menu.get_children():
            self.__menu.remove(child)

    def plugin_songs(self, songs):
        return True
Example #19
0
 def test_any_song_multiple(self):
     FakeSongsMenuPlugin.plugin_handles = any_song(even, never)
     p = FakeSongsMenuPlugin(self.songs, None)
     self.failIf(p.plugin_handles(self.songs))
     self.failIf(p.plugin_handles(self.songs[:1]))