# HG changeset patch # User IBBoard # Date 1510417642 0 # Node ID 4fab77a9ac9d106f4eb6cc1e8fb2d2bfee6237d7 Initial commit of working script diff -r 000000000000 -r 4fab77a9ac9d import-rating-rhythmbox.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/import-rating-rhythmbox.py Sat Nov 11 16:27:22 2017 +0000 @@ -0,0 +1,124 @@ +#! /usr/bin/env python3 + +from xml.dom.minidom import parse +import sys +from os import path +from os.path import expanduser +from urllib.parse import urlparse, unquote +import mutagen + + +### Usage: python3 ./quodlibet-to-rhythmbox.py > output.xml; \ +### mv output.xml ~/.local/share/rhythmbox/rhythmdb.xml + + +# List of sub-keys that we will take POPM ratings from +# The order they are here is their priority - we take the first one we find +TRANSFER_KEYS = [ + 'quodlibet@lists.sacredchao.net', + 'Banshee' +] + +DEBUG_LEVEL = 0 + + +def _debug(message, level=1): + if level <= DEBUG_LEVEL: + print(message, file=sys.stderr) + + +def _get_text(nodelist): + content = [] + for node in nodelist: + if node.nodeType == node.TEXT_NODE: + content.append(node.data) + return ''.join(content) + + +def _get_path_from_url(file_uri): + parsed = urlparse(file_uri) + return path.abspath(path.join(parsed.netloc, unquote(parsed.path))) + + +def _normalise_rating(rating): + # Take the Banshee approach to ratings, but assume that a rating of 5 + # or below is an exact rating (normally a rogue Banshee rating) + # https://git.gnome.org/browse/banshee/tree/src/Core/Banshee.Core/Banshee.Streaming/StreamRatingTagger.cs#n54 + if rating <= 5: + return rating + elif rating < 64: + return 1 + elif rating < 128: + return 2 + elif rating < 192: + return 3 + elif rating < 255: + return 4 + else: + return 5 + + +def _set_rating(doc, entry, value): + rating = _normalise_rating(value) + _debug("Setting rating to {} based on value {}".format(rating, value)) + rating_tag = doc.createElement("rating") + rating_tag.appendChild(doc.createTextNode(str(rating))) + entry.appendChild(rating_tag) + +def transfer_ratings(): + """ + Transfers the ratings from IDv3 POPM tags to Rhythmbox's XML database + and prints the resulting XML to stdout + """ + home = expanduser("~") + + doc_path = path.join(home, '.local/share/rhythmbox/rhythmdb.xml') + doc = parse(doc_path) + + if len(doc.childNodes) != 1: + _debug("Invalid document structure", 0) + exit(1) + + rhythmdb = doc.childNodes[0] + + if rhythmdb.nodeName != 'rhythmdb': + _debug("Invalid document structure", 0) + exit(1) + + for entry in rhythmdb.childNodes: + if entry.nodeType == entry.TEXT_NODE or entry.getAttribute('type') != 'song': + continue + if len(entry.getElementsByTagName('rating')) > 0: + continue + + locations = entry.getElementsByTagName('location') + + if len(locations) != 1: + _debug("Song did not have one location", 0) + continue + + location = locations[0] + file_path = _get_path_from_url(_get_text(location.childNodes)) + + if not path.isfile(file_path): + _debug("{} was not a file".format(file_path)) + + try: + track = mutagen.File(file_path) + + for key_suffix in TRANSFER_KEYS: + key = 'POPM:{}'.format(key_suffix) + if key in track.tags: + rating = track.tags[key].rating + if rating > 0: + _debug("Found rating from {} for {}".format(key_suffix, file_path)) + _set_rating(doc, entry, track.tags[key].rating) + break + except mutagen.MutagenError as ex: + _debug("Error processing {}: {}".format(file_path, ex), 0) + + print(doc.toxml()) + + +if __name__ == '__main__': + transfer_ratings()