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()