Does Rhythmbox's inability to use xattrs to store ratings bug you?

Jan 23, 2009 17:13


If so, first, you're a hopeless geek. Second, I've written a little Python script to work around the issue. I'll post another one that reads the ratings and rewrites Rhythmbox's database with them in a while.

This little Python script reads Rhythmbox's XML ratings database and stores the ratings as user.rating attributes on the inodes of any music files with a local ("file:///") url. Since I render my music read-only normally, it also sets the user-write bit on the file before changing the xattr and clears it afterwards.

You will need the xattr and libxml2 modules installed. On Debian/Ubuntu, that's the python-xattr and python-libxml2 packages. The target file system must also support user xattrs. On ext3, that means adding the user_xattr mount option to the filesystem's entry in /etc/fstab and running mount /path/to/mount/point -o remount (or rebooting) to enable them.

You can see the ratings with the getfattr tool from the attr package (among other things):

craig@ayaki:~/flac/Modest Mouse/We Were Dead$ getfattr -d * # file: 02\040Dashboard.flac user.rating="2" # file: 03\040Fire\040It\040Up.flac user.rating="3" # file: 06\040Missed\040The\040Boat.flac user.rating="4"
Here's the script:

#!/usr/bin/env python # # Scan the Rhythmbox database: # $HOME/.gnome2/rhythmbox/rhythmdb.xml # and for each file on the local machine add a `rating' user xattr # on the file. # # Requires the python-libxml2 and python-xattr modules. # import sys import os import xattr import libxml2 import urllib import urlparse def get_rating_attr(fn): try: return xattr.getxattr(fn, 'user.rating') except IOError,e: if e.errno == 61: # no data available return None else: raise def set_rating_attr(fn, rating): oldmode = os.stat(fn)[0] os.chmod(fn, oldmode & 0600) xattr.setxattr(fn, 'user.rating', str(rating)) os.chmod(fn, oldmode) class RhythmRatings(object): def __init__(self, dbname): self.dbname = dbname self.doc = libxml2.parseFile(self.dbname) # Find song elements with rating and location children rated_songs_xpath = '//rhythmdb/entry[child::rating][child::location][@type=\'song\']' ratings = [ x.children.content for x in self.doc.xpathEval(rated_songs_xpath + '/rating') ] locations = [ x.children.content for x in self.doc.xpathEval(rated_songs_xpath + '/location') ] assert(len(ratings) == len(locations)) self.rating_info = [ (urllib.unquote(self._url_file(x[0])), x[1]) for x in zip(locations, ratings) if self._is_local_url(x[0]) ] print "Loaded %s rated local files from db" % len(self.rating_info) def _is_local_url(self, url): u = urlparse.urlsplit(url) return u[0] == 'file' def _url_file(self, url): u = urlparse.urlsplit(url) return u[2] def __iter__(self): for x in self.rating_info: yield x def main(): checked = 0 labeled = 0 failed = 0 for (fn, rating) in RhythmRatings(os.environ['HOME'] + '/.gnome2/rhythmbox/rhythmdb.xml'): checked += 1 if get_rating_attr(fn) != rating: try: set_rating_attr(fn, rating); labeled += 1 except IOError,e: failed += 1 print "Checked ", checked, " files" print "Labeled ", labeled, " files" if failed: print "Failed to label ", failed, " files" if __name__ == '__main__': main()

programming, linux, music

Previous post Next post
Up