0
|
1 #! /usr/bin/env python3
|
|
2
|
|
3 from xml.dom.minidom import parse
|
|
4 import sys
|
|
5 from os import path
|
|
6 from os.path import expanduser
|
|
7 from urllib.parse import urlparse, unquote
|
|
8 import mutagen
|
|
9
|
|
10
|
|
11 ### Usage: python3 ./quodlibet-to-rhythmbox.py > output.xml; \
|
|
12 ### mv output.xml ~/.local/share/rhythmbox/rhythmdb.xml
|
|
13
|
|
14
|
|
15 # List of sub-keys that we will take POPM ratings from
|
|
16 # The order they are here is their priority - we take the first one we find
|
|
17 TRANSFER_KEYS = [
|
|
18 'quodlibet@lists.sacredchao.net',
|
|
19 'Banshee'
|
|
20 ]
|
|
21
|
|
22 DEBUG_LEVEL = 0
|
|
23
|
|
24
|
|
25 def _debug(message, level=1):
|
|
26 if level <= DEBUG_LEVEL:
|
|
27 print(message, file=sys.stderr)
|
|
28
|
|
29
|
|
30 def _get_text(nodelist):
|
|
31 content = []
|
|
32 for node in nodelist:
|
|
33 if node.nodeType == node.TEXT_NODE:
|
|
34 content.append(node.data)
|
|
35 return ''.join(content)
|
|
36
|
|
37
|
|
38 def _get_path_from_url(file_uri):
|
|
39 parsed = urlparse(file_uri)
|
|
40 return path.abspath(path.join(parsed.netloc, unquote(parsed.path)))
|
|
41
|
|
42
|
|
43 def _normalise_rating(rating):
|
|
44 # Take the Banshee approach to ratings, but assume that a rating of 5
|
|
45 # or below is an exact rating (normally a rogue Banshee rating)
|
|
46 # https://git.gnome.org/browse/banshee/tree/src/Core/Banshee.Core/Banshee.Streaming/StreamRatingTagger.cs#n54
|
|
47 if rating <= 5:
|
|
48 return rating
|
|
49 elif rating < 64:
|
|
50 return 1
|
|
51 elif rating < 128:
|
|
52 return 2
|
|
53 elif rating < 192:
|
|
54 return 3
|
|
55 elif rating < 255:
|
|
56 return 4
|
|
57 else:
|
|
58 return 5
|
|
59
|
|
60
|
|
61 def _set_rating(doc, entry, value):
|
|
62 rating = _normalise_rating(value)
|
|
63 _debug("Setting rating to {} based on value {}".format(rating, value))
|
|
64 rating_tag = doc.createElement("rating")
|
|
65 rating_tag.appendChild(doc.createTextNode(str(rating)))
|
|
66 entry.appendChild(rating_tag)
|
|
67
|
|
68 def transfer_ratings():
|
|
69 """
|
|
70 Transfers the ratings from IDv3 POPM tags to Rhythmbox's XML database
|
|
71 and prints the resulting XML to stdout
|
|
72 """
|
|
73 home = expanduser("~")
|
|
74
|
|
75 doc_path = path.join(home, '.local/share/rhythmbox/rhythmdb.xml')
|
|
76 doc = parse(doc_path)
|
|
77
|
|
78 if len(doc.childNodes) != 1:
|
|
79 _debug("Invalid document structure", 0)
|
|
80 exit(1)
|
|
81
|
|
82 rhythmdb = doc.childNodes[0]
|
|
83
|
|
84 if rhythmdb.nodeName != 'rhythmdb':
|
|
85 _debug("Invalid document structure", 0)
|
|
86 exit(1)
|
|
87
|
|
88 for entry in rhythmdb.childNodes:
|
|
89 if entry.nodeType == entry.TEXT_NODE or entry.getAttribute('type') != 'song':
|
|
90 continue
|
|
91 if len(entry.getElementsByTagName('rating')) > 0:
|
|
92 continue
|
|
93
|
|
94 locations = entry.getElementsByTagName('location')
|
|
95
|
|
96 if len(locations) != 1:
|
|
97 _debug("Song did not have one location", 0)
|
|
98 continue
|
|
99
|
|
100 location = locations[0]
|
|
101 file_path = _get_path_from_url(_get_text(location.childNodes))
|
|
102
|
|
103 if not path.isfile(file_path):
|
|
104 _debug("{} was not a file".format(file_path))
|
|
105
|
|
106 try:
|
|
107 track = mutagen.File(file_path)
|
|
108
|
|
109 for key_suffix in TRANSFER_KEYS:
|
|
110 key = 'POPM:{}'.format(key_suffix)
|
|
111 if key in track.tags:
|
|
112 rating = track.tags[key].rating
|
|
113 if rating > 0:
|
|
114 _debug("Found rating from {} for {}".format(key_suffix, file_path))
|
|
115 _set_rating(doc, entry, track.tags[key].rating)
|
|
116 break
|
|
117 except mutagen.MutagenError as ex:
|
|
118 _debug("Error processing {}: {}".format(file_path, ex), 0)
|
|
119
|
|
120 print(doc.toxml())
|
|
121
|
|
122
|
|
123 if __name__ == '__main__':
|
|
124 transfer_ratings()
|