def test_artwork_url(self):
     lib = SoundcloudLibrary(self.FakeClient())
     lib.query_with_refresh("")
     s = listvalues(lib._contents)[0]
     self.failUnlessEqual(
         s("artwork_url"),
         "https://i1.sndcdn.com/artworks-000108682375-q4j7y6-t500x500.jpg")
Example #2
0
    def __invoke(self, librarian, event, *args):
        args = list(args)
        if args and args[0]:
            if isinstance(args[0], dict):
                args[0] = SongWrapper(args[0])
            elif isinstance(args[0], (set, list)):
                args[0] = ListWrapper(args[0])
        for plugin in listvalues(self.__plugins):
            method_name = 'plugin_on_' + event.replace('-', '_')
            handler = getattr(plugin, method_name, None)

            def overridden(obj, name):
                return name in type(obj).__dict__

            if overridden(plugin, method_name):
                try:
                    handler(*args)
                except Exception:
                    print_e("Error during %s on %s" %
                            (method_name, type(plugin)))
                    errorhook()

        if event not in ["removed", "changed"] and args:
            from quodlibet import app
            songs = args[0]
            if not isinstance(songs, (set, list)):
                songs = [songs]
            songs = filter(None, songs)
            check_wrapper_changed(librarian, app.window, songs)
Example #3
0
    def _delayed_recalc(self):
        self._timeout = None

        tv = self.get_tree_view()
        assert tv is not None
        range_ = tv.get_visible_range()
        if not range_:
            return

        start, end = range_
        start = start[0]
        end = end[0]

        # compute the cell width for all drawn cells in range +/- 3
        for key, value in listitems(self._texts):
            if not (start - 3) <= key <= (end + 3):
                del self._texts[key]
            elif isinstance(value, string_types):
                self._texts[key] = self._cell_width(value)

        # resize if too small or way too big and above the minimum
        width = self.get_width()
        needed_width = max([self._get_min_width()] + listvalues(self._texts))
        if width < needed_width:
            self._resize(needed_width)
        elif width - needed_width >= self._cell_width("0"):
            self._resize(needed_width)
Example #4
0
    def _delayed_recalc(self):
        self._timeout = None

        tv = self.get_tree_view()
        assert tv is not None
        range_ = tv.get_visible_range()
        if not range_:
            return

        start, end = range_
        start = start[0]
        end = end[0]

        # compute the cell width for all drawn cells in range +/- 3
        for key, value in listitems(self._texts):
            if not (start - 3) <= key <= (end + 3):
                del self._texts[key]
            elif isinstance(value, string_types):
                self._texts[key] = self._cell_width(value)

        # resize if too small or way too big and above the minimum
        width = self.get_width()
        needed_width = max([self._get_min_width()] + listvalues(self._texts))
        if width < needed_width:
            self._resize(needed_width)
        elif width - needed_width >= self._cell_width("0"):
            self._resize(needed_width)
Example #5
0
    def __invoke(self, librarian, event, *args):
        args = list(args)
        if args and args[0]:
            if isinstance(args[0], dict):
                args[0] = SongWrapper(args[0])
            elif isinstance(args[0], (set, list)):
                args[0] = ListWrapper(args[0])
        for plugin in listvalues(self.__plugins):
            method_name = 'plugin_on_' + event.replace('-', '_')
            handler = getattr(plugin, method_name, None)

            def overridden(obj, name):
                return name in type(obj).__dict__

            if overridden(plugin, method_name):
                try:
                    handler(*args)
                except Exception:
                    print_e("Error during %s on %s" %
                            (method_name, type(plugin)))
                    errorhook()

        if event not in ["removed", "changed"] and args:
            from quodlibet import app
            songs = args[0]
            if not isinstance(songs, (set, list)):
                songs = [songs]
            songs = filter(None, songs)
            check_wrapper_changed(librarian, app.window, songs)
Example #6
0
 def test_artwork_url(self):
     lib = SoundcloudLibrary(self.FakeClient())
     lib.query_with_refresh("")
     s = listvalues(lib._contents)[0]
     self.failUnlessEqual(
         s("artwork_url"),
         "https://i1.sndcdn.com/artworks-000108682375-q4j7y6-t500x500.jpg")
Example #7
0
    def rebuild(self, paths, force=False, exclude=[], cofuncid=None):
        """Reload or remove songs if they have changed or been deleted.

        This generator rebuilds the library over the course of iteration.

        Any paths given will be scanned for new files, using the 'scan'
        method.

        Only items present in the library when the rebuild is started
        will be checked.

        If this function is copooled, set "cofuncid" to enable pause/stop
        buttons in the UI.
        """

        print_d("Rebuilding, force is %s." % force, self)

        task = Task(_("Library"), _("Checking mount points"))
        if cofuncid:
            task.copool(cofuncid)
        for i, (point, items) in task.list(enumerate(self._masked.items())):
            if ismount(point):
                self._contents.update(items)
                del(self._masked[point])
                self.emit('added', listvalues(items))
                yield True

        task = Task(_("Library"), _("Scanning library"))
        if cofuncid:
            task.copool(cofuncid)
        changed, removed = set(), set()
        for i, (key, item) in task.list(enumerate(sorted(self.items()))):
            if key in self._contents and force or not item.valid():
                self.reload(item, changed, removed)
                # These numbers are pretty empirical. We should yield more
            # often than we emit signals; that way the main loop stays
            # interactive and doesn't get bogged down in updates.
            if len(changed) > 100:
                self.emit('changed', changed)
                changed = set()
            if len(removed) > 100:
                self.emit('removed', removed)
                removed = set()
            if len(changed) > 5 or i % 100 == 0:
                yield True
        print_d("Removing %d, changing %d." % (len(removed), len(changed)),
                self)
        if removed:
            self.emit('removed', removed)
        if changed:
            self.emit('changed', changed)

        for value in self.scan(paths, exclude, cofuncid):
            yield value
Example #8
0
    def rebuild(self, paths, force=False, exclude=[], cofuncid=None):
        """Reload or remove songs if they have changed or been deleted.

        This generator rebuilds the library over the course of iteration.

        Any paths given will be scanned for new files, using the 'scan'
        method.

        Only items present in the library when the rebuild is started
        will be checked.

        If this function is copooled, set "cofuncid" to enable pause/stop
        buttons in the UI.
        """

        print_d("Rebuilding, force is %s." % force, self)

        task = Task(_("Library"), _("Checking mount points"))
        if cofuncid:
            task.copool(cofuncid)
        for i, (point, items) in task.list(enumerate(self._masked.items())):
            if ismount(point):
                self._contents.update(items)
                del (self._masked[point])
                self.emit('added', listvalues(items))
                yield True

        task = Task(_("Library"), _("Scanning library"))
        if cofuncid:
            task.copool(cofuncid)
        changed, removed = set(), set()
        for i, (key, item) in task.list(enumerate(sorted(self.items()))):
            if key in self._contents and force or not item.valid():
                self.reload(item, changed, removed)
                # These numbers are pretty empirical. We should yield more
            # often than we emit signals; that way the main loop stays
            # interactive and doesn't get bogged down in updates.
            if len(changed) > 100:
                self.emit('changed', changed)
                changed = set()
            if len(removed) > 100:
                self.emit('removed', removed)
                removed = set()
            if len(changed) > 5 or i % 100 == 0:
                yield True
        print_d("Removing %d, changing %d." % (len(removed), len(changed)),
                self)
        if removed:
            self.emit('removed', removed)
        if changed:
            self.emit('changed', changed)

        for value in self.scan(paths, exclude, cofuncid):
            yield value
Example #9
0
    def __get_albums(self, songs):
        albums = {}
        for song in songs:
            key = song.album_key
            if key not in albums:
                albums[key] = []
            albums[key].append(song)

        albums = listvalues(albums)
        for album in albums:
            album.sort()
        return albums
Example #10
0
    def __get_albums(self, songs):
        albums = {}
        for song in songs:
            key = song.album_key
            if key not in albums:
                albums[key] = []
            albums[key].append(song)

        albums = listvalues(albums)
        for album in albums:
            album.sort()
        return albums
Example #11
0
    def get_content(self):
        """Return visible and masked items"""

        items = listvalues(self)
        for masked in self._masked.values():
            items.extend(masked.values())

        # Item keys are often based on filenames, in which case
        # sorting takes advantage of the filesystem cache when we
        # reload/rescan the files.
        items.sort(key=lambda item: item.key)

        return items
Example #12
0
    def get_content(self):
        """Return visible and masked items"""

        items = listvalues(self)
        for masked in self._masked.values():
            items.extend(masked.values())

        # Item keys are often based on filenames, in which case
        # sorting takes advantage of the filesystem cache when we
        # reload/rescan the files.
        items.sort(key=lambda item: item.key)

        return items
Example #13
0
 def test_parse(self):
     lib = self.lib
     lib.query_with_refresh("dummy search")
     songs = listvalues(lib._contents)
     self.failUnlessEqual(len(songs), 1)
     s = songs[0]
     self.failUnlessEqual(s("artist"), "Kerstin Eden")
     self.failUnlessEqual(s("date"), "2015-03-02")
     self.failUnlessEqual(s("~#download_count"), 2062)
     self.failUnlessEqual(s("~#playback_count"), 23656)
     self.failUnlessEqual(s("~#favoritings_count"), 882)
     self.failUnlessEqual(s("~#rating"), 1.0)
     self.failUnlessEqual(s("~#playcount"), 4)
     assert int(s("~#bitrate")) == 319
 def test_parse(self):
     lib = self.lib
     lib.query_with_refresh("dummy search")
     songs = listvalues(lib._contents)
     self.failUnlessEqual(len(songs), 1)
     s = songs[0]
     self.failUnlessEqual(s("artist"), "Kerstin Eden")
     self.failUnlessEqual(s("date"), "2015-03-02")
     self.failUnlessEqual(s("~#download_count"), 2062)
     self.failUnlessEqual(s("~#playback_count"), 23656)
     self.failUnlessEqual(s("~#favoritings_count"), 882)
     self.failUnlessEqual(s("~#rating"), 1.0)
     self.failUnlessEqual(s("~#playcount"), 4)
     assert int(s("~#bitrate")) == 319
    def test_utils(self):
        model = CollectionTreeStore()
        model.set_albums([("~people", 0)], self.albums)
        a = listvalues(self.albums)
        a.sort(key=lambda x: x.key)

        path = model.get_path_for_album(a[0])
        albums = model.get_albums_for_path(path)
        self.failUnless(a[0] in albums)

        albums = model.get_albums_for_iter(model.get_iter(path))
        self.failUnless(a[0] in albums)

        x = model.get_album(model.get_iter_first())
        self.failIf(x)
        x = model.get_album(model.get_iter(path))
        self.failUnlessEqual(x, a[0])

        for r in model:
            self.failUnless(model.get_markup(model.tags, r.iter))

        x = list(model.iter_albums(None))
        self.assertEqual(set(x), set(a))
    def test_utils(self):
        model = CollectionTreeStore()
        model.set_albums([("~people", 0)], self.albums)
        a = listvalues(self.albums)
        a.sort(key=lambda x: x.key)

        path = model.get_path_for_album(a[0])
        albums = model.get_albums_for_path(path)
        self.failUnless(a[0] in albums)

        albums = model.get_albums_for_iter(model.get_iter(path))
        self.failUnless(a[0] in albums)

        x = model.get_album(model.get_iter_first())
        self.failIf(x)
        x = model.get_album(model.get_iter(path))
        self.failUnlessEqual(x, a[0])

        for r in model:
            self.failUnless(model.get_markup(model.tags, r.iter))

        x = list(model.iter_albums(None))
        self.assertEqual(set(x), set(a))
Example #17
0
    def get_masked(self, mount_point):
        """List of items for a mount point"""

        return listvalues(self._masked.get(mount_point, {}))
Example #18
0
    def write(self):
        with translate_errors():
            audio = self.Kind(self['~filename'])

        if audio.tags is None:
            audio.add_tags()
        tag = audio.tags

        # prefill TMCL with the ones we can't read
        mcl = tag.get("TMCL", mutagen.id3.TMCL(encoding=3, people=[]))
        mcl.people = [(r, n) for (r, n) in mcl.people
                      if not self.__validate_name(r)]

        # delete all TXXX/COMM we can read except empty COMM
        for frame in ["COMM:", "TXXX:"]:
            for t in tag.getall(frame + "QuodLibet:"):
                if t.desc and self.__validate_name(t.desc):
                    del tag[t.HashKey]

        for key in [
                "UFID:http://musicbrainz.org", "TMCL",
                "POPM:%s" % const.EMAIL,
                "POPM:%s" % config.get("editing", "save_email")
        ]:
            if key in tag:
                del (tag[key])

        for key, id3name in self.SDI.items():
            tag.delall(id3name)
            if key not in self:
                continue
            enc = encoding_for(self[key])
            Kind = mutagen.id3.Frames[id3name]
            text = self[key].split("\n")
            if id3name == "WOAR":
                for t in text:
                    tag.add(Kind(url=t))
            else:
                tag.add(Kind(encoding=enc, text=text))

        dontwrite = ["genre", "comment", "musicbrainz_trackid", "lyrics"] \
            + RG_KEYS + listvalues(self.TXXX_MAP)

        if "musicbrainz_trackid" in self.realkeys():
            f = mutagen.id3.UFID(
                owner="http://musicbrainz.org",
                data=self["musicbrainz_trackid"].encode("utf-8"))
            tag.add(f)

        # Issue 439 - Only write valid ISO 639-2 codes to TLAN (else TXXX)
        tag.delall("TLAN")
        if "language" in self:
            langs = self["language"].split("\n")
            if all([lang in ISO_639_2 for lang in langs]):
                # Save value(s) to TLAN tag. Guaranteed to be ASCII here
                tag.add(mutagen.id3.TLAN(encoding=3, text=langs))
                dontwrite.append("language")
            else:
                print_d("Not using invalid language code '%s' in TLAN" %
                        self["language"])

        # Filter out known keys, and ones set not to write [generically].
        keys_to_write = filter(lambda k: not (k in self.SDI or k in dontwrite),
                               self.realkeys())
        for key in keys_to_write:
            enc = encoding_for(self[key])
            if key.startswith("performer:"):
                mcl.people.append([key.split(":", 1)[1], self[key]])
                continue

            f = mutagen.id3.TXXX(encoding=enc,
                                 text=self[key].split("\n"),
                                 desc=u"QuodLibet::%s" % key)
            tag.add(f)

        if mcl.people:
            tag.add(mcl)

        if "genre" in self:
            enc = encoding_for(self["genre"])
            t = self["genre"].split("\n")
            tag.add(mutagen.id3.TCON(encoding=enc, text=t))
        else:
            try:
                del (tag["TCON"])
            except KeyError:
                pass

        tag.delall("COMM:")
        if "comment" in self:
            enc = encoding_for(self["comment"])
            t = self["comment"].split("\n")
            tag.add(
                mutagen.id3.COMM(encoding=enc,
                                 text=t,
                                 desc=u"",
                                 lang="\x00\x00\x00"))

        tag.delall("USLT::\x00\x00\x00")
        if "lyrics" in self:
            enc = encoding_for(self["lyrics"])
            # lyrics are single string, not array
            tag.add(
                mutagen.id3.USLT(encoding=enc,
                                 text=self["lyrics"],
                                 desc=u"",
                                 lang="\x00\x00\x00"))

        # Delete old foobar replaygain ..
        for frame in tag.getall("TXXX"):
            if frame.desc.lower() in RG_KEYS:
                del tag[frame.HashKey]

        # .. write new one
        for k in RG_KEYS:
            # Add new ones
            if k in self:
                value = self[k]
                tag.add(
                    mutagen.id3.TXXX(encoding=encoding_for(value),
                                     text=value.split("\n"),
                                     desc=k))

        # we shouldn't delete all, but we use unknown ones as fallback, so make
        # sure they don't come back after reloading
        for t in tag.getall("RVA2"):
            if t.channel == 1:
                del tag[t.HashKey]

        for k in ["track", "album"]:
            if ('replaygain_%s_gain' % k) in self:
                try:
                    gain = float(self["replaygain_%s_gain" % k].split()[0])
                except (ValueError, IndexError):
                    gain = 0
                try:
                    peak = float(self["replaygain_%s_peak" % k])
                except (ValueError, KeyError):
                    peak = 0
                # https://github.com/quodlibet/quodlibet/issues/1027
                peak = max(min(1.9, peak), 0)
                gain = max(min(63.9, gain), -64)
                f = mutagen.id3.RVA2(desc=k, channel=1, gain=gain, peak=peak)
                tag.add(f)

        for key in self.TXXX_MAP:
            try:
                del (tag["TXXX:" + key])
            except KeyError:
                pass
        for key in self.PAM_XXXT:
            if key in self.SDI:
                # we already write it back using non-TXXX frames
                continue
            if key in self:
                value = self[key]
                f = mutagen.id3.TXXX(encoding=encoding_for(value),
                                     text=value.split("\n"),
                                     desc=self.PAM_XXXT[key])
                tag.add(f)

        if (config.getboolean("editing", "save_to_songs")
                and (self.has_rating or self.get("~#playcount", 0) != 0)):
            email = config.get("editing", "save_email").strip()
            email = email or const.EMAIL
            t = mutagen.id3.POPM(email=email,
                                 rating=int(255 * self("~#rating")),
                                 count=self.get("~#playcount", 0))
            tag.add(t)

        with translate_errors():
            audio.save()
        self.sanitize()
Example #19
0
    def write(self):
        with translate_errors():
            audio = self.Kind(self['~filename'])

        if audio.tags is None:
            audio.add_tags()
        tag = audio.tags

        # prefill TMCL with the ones we can't read
        mcl = tag.get("TMCL", mutagen.id3.TMCL(encoding=3, people=[]))
        mcl.people = [(r, n) for (r, n) in mcl.people
                      if not self.__validate_name(r)]

        # delete all TXXX/COMM we can read except empty COMM
        for frame in ["COMM:", "TXXX:"]:
            for t in tag.getall(frame + "QuodLibet:"):
                if t.desc and self.__validate_name(t.desc):
                    del tag[t.HashKey]

        for key in ["UFID:http://musicbrainz.org",
                    "TMCL",
                    "POPM:%s" % const.EMAIL,
                    "POPM:%s" % config.get("editing", "save_email")]:
            if key in tag:
                del(tag[key])

        for key, id3name in self.SDI.items():
            tag.delall(id3name)
            if key not in self:
                continue
            enc = encoding_for(self[key])
            Kind = mutagen.id3.Frames[id3name]
            text = self[key].split("\n")
            if id3name == "WOAR":
                for t in text:
                    tag.add(Kind(url=t))
            else:
                tag.add(Kind(encoding=enc, text=text))

        dontwrite = ["genre", "comment", "musicbrainz_trackid", "lyrics"] \
            + RG_KEYS + listvalues(self.TXXX_MAP)

        if "musicbrainz_trackid" in self.realkeys():
            f = mutagen.id3.UFID(
                owner="http://musicbrainz.org",
                data=self["musicbrainz_trackid"].encode("utf-8"))
            tag.add(f)

        # Issue 439 - Only write valid ISO 639-2 codes to TLAN (else TXXX)
        tag.delall("TLAN")
        if "language" in self:
            langs = self["language"].split("\n")
            if all([lang in ISO_639_2 for lang in langs]):
                # Save value(s) to TLAN tag. Guaranteed to be ASCII here
                tag.add(mutagen.id3.TLAN(encoding=3, text=langs))
                dontwrite.append("language")
            else:
                print_d("Not using invalid language code '%s' in TLAN" %
                        self["language"])

        # Filter out known keys, and ones set not to write [generically].
        keys_to_write = filter(lambda k: not (k in self.SDI or k in dontwrite),
                               self.realkeys())
        for key in keys_to_write:
            enc = encoding_for(self[key])
            if key.startswith("performer:"):
                mcl.people.append([key.split(":", 1)[1], self[key]])
                continue

            f = mutagen.id3.TXXX(
                encoding=enc, text=self[key].split("\n"),
                desc=u"QuodLibet::%s" % key)
            tag.add(f)

        if mcl.people:
            tag.add(mcl)

        if "genre" in self:
            enc = encoding_for(self["genre"])
            t = self["genre"].split("\n")
            tag.add(mutagen.id3.TCON(encoding=enc, text=t))
        else:
            try:
                del(tag["TCON"])
            except KeyError:
                pass

        tag.delall("COMM:")
        if "comment" in self:
            enc = encoding_for(self["comment"])
            t = self["comment"].split("\n")
            tag.add(mutagen.id3.COMM(encoding=enc, text=t, desc=u"",
                                     lang="\x00\x00\x00"))

        tag.delall("USLT::\x00\x00\x00")
        if "lyrics" in self:
            enc = encoding_for(self["lyrics"])
            # lyrics are single string, not array
            tag.add(mutagen.id3.USLT(encoding=enc, text=self["lyrics"],
                                     desc=u"", lang="\x00\x00\x00"))

        # Delete old foobar replaygain ..
        for frame in tag.getall("TXXX"):
            if frame.desc.lower() in RG_KEYS:
                del tag[frame.HashKey]

        # .. write new one
        for k in RG_KEYS:
            # Add new ones
            if k in self:
                value = self[k]
                tag.add(mutagen.id3.TXXX(encoding=encoding_for(value),
                                         text=value.split("\n"),
                                         desc=k))

        # we shouldn't delete all, but we use unknown ones as fallback, so make
        # sure they don't come back after reloading
        for t in tag.getall("RVA2"):
            if t.channel == 1:
                del tag[t.HashKey]

        for k in ["track", "album"]:
            if ('replaygain_%s_gain' % k) in self:
                try:
                    gain = float(self["replaygain_%s_gain" % k].split()[0])
                except ValueError:
                    gain = 0
                try:
                    peak = float(self["replaygain_%s_peak" % k])
                except (ValueError, KeyError):
                    peak = 0
                # https://github.com/quodlibet/quodlibet/issues/1027
                peak = max(min(1.9, peak), 0)
                gain = max(min(63.9, gain), -64)
                f = mutagen.id3.RVA2(desc=k, channel=1, gain=gain, peak=peak)
                tag.add(f)

        for key in self.TXXX_MAP:
            try:
                del(tag["TXXX:" + key])
            except KeyError:
                pass
        for key in self.PAM_XXXT:
            if key in self.SDI:
                # we already write it back using non-TXXX frames
                continue
            if key in self:
                value = self[key]
                f = mutagen.id3.TXXX(encoding=encoding_for(value),
                                     text=value.split("\n"),
                                     desc=self.PAM_XXXT[key])
                tag.add(f)

        if (config.getboolean("editing", "save_to_songs") and
                (self.has_rating or self.get("~#playcount", 0) != 0)):
            email = config.get("editing", "save_email").strip()
            email = email or const.EMAIL
            t = mutagen.id3.POPM(email=email,
                                 rating=int(255 * self("~#rating")),
                                 count=self.get("~#playcount", 0))
            tag.add(t)

        with translate_errors():
            audio.save()
        self.sanitize()
Example #20
0
    def get_content(self):
        """All items including hidden ones for saving the library
           (see FileLibrary with masked items)
        """

        return listvalues(self)
Example #21
0
    def get_masked(self, mount_point):
        """List of items for a mount point"""

        return listvalues(self._masked.get(mount_point, {}))
Example #22
0
    def get_content(self):
        """All items including hidden ones for saving the library
           (see FileLibrary with masked items)
        """

        return listvalues(self)