view import-rating-rhythmbox.py @ 0:4fab77a9ac9d default tip

Initial commit of working script
author IBBoard <dev@ibboard.co.uk>
date Sat, 11 Nov 2017 16:27:22 +0000
parents
children
line wrap: on
line source

#! /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()