Last active
August 4, 2025 21:13
-
-
Save Teque5/69d5ece2003e7ea1f3c29daf01b411a2 to your computer and use it in GitHub Desktop.
This script adds ratings from flac and mp3 files to Rhythmbox format while preserving the existing database. Rhythmbox uses XML database to store its music files, together with rating. However, it does not read the ID3 rating frame of a music file if it exists, making it harder for people transferring rated music from other player to this one (f…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
popm2rhythmbox | |
Convert saved audio file rating formats | |
┌──────┐ ┌─────────┐ | |
│Winamp├───►RhythmBox│ | |
└──────┘ └─────────┘ | |
inspired by https://github.com/shatterhand19/RhythmboxPOPM | |
""" | |
import os | |
from urllib.parse import unquote | |
from alive_progress import alive_bar | |
from blessings import Terminal | |
from lxml import etree | |
from mutagen.flac import FLAC | |
from mutagen.id3 import ID3 | |
from mutagen.mp4 import MP4 | |
# from mutagen.oggopus import OggOpus | |
FLAC_MAP = { | |
# same as MP4 tags | |
"20": 1, | |
"40": 2, | |
"60": 3, | |
"80": 4, | |
"100": 5, | |
} | |
ID3_MAP = { | |
1: 1, | |
64: 2, | |
128: 3, | |
196: 4, | |
255: 5, | |
} | |
DATABASE = os.path.expanduser("~/.local/share/rhythmbox/rhythmdb.xml") | |
def read_id3(location: str) -> int: | |
"""return star count based on ID3 tag""" | |
tag = ID3(location) | |
raw = tag["POPM:[email protected]"].rating | |
return ID3_MAP[raw] | |
def read_flac(location: str) -> int: | |
"""return star count based on FLAC tag""" | |
tag = FLAC(location) | |
raw = tag["rating"][0] | |
return FLAC_MAP[raw] | |
def read_mp4(location: str) -> int: | |
"""return star count based on MP4 tag""" | |
tag = MP4(location) | |
raw = tag["rate"][0] | |
return FLAC_MAP[raw] | |
if __name__ == "__main__": | |
""" | |
Read WINAMP compatible ratings stored to files into Rhythmbox XML library | |
Files are expected to be in the Rhythmbox database before running this script. | |
TODO: Finish adding OggOpus, need tag field name | |
""" | |
verbose = 0 # 1 for info, 2 for debug | |
t = Terminal() # pretty colors | |
print(t.bold("popm2rhythmbox\n")) | |
tree = etree.parse(DATABASE) | |
entries = tree.findall("entry") | |
with alive_bar( | |
len(entries), | |
title="reading tags", | |
bar="filling", | |
spinner="notes2", | |
enrich_print=False, | |
) as bar: | |
for song in entries: | |
rating = None | |
location = unquote(song.find("location").text).replace("file://", "") | |
if not os.path.exists(location): | |
# file does not exist, skip | |
if verbose == 2: | |
print(t.red("File does not exist:"), t.bold(location)) | |
bar() | |
continue | |
filename = location.rpartition("/")[-1] | |
filetype = location.rpartition(".")[-1] | |
try: | |
if filetype == "mp3": | |
rating = read_id3(location) | |
if verbose: | |
print(t.bold(str(rating)), t.cyan(filename)) | |
elif filetype == "flac": | |
rating = read_flac(location) | |
if verbose: | |
print(t.bold(str(rating)), t.magenta(filename)) | |
elif filetype in ["m4a", "mp4"]: | |
rating = read_mp4(location) | |
if verbose: | |
print(t.bold(str(rating)), t.yellow(filename)) | |
else: | |
if verbose == 2: | |
print(t.bold("x"), "File type not recognized:", location) | |
except KeyError: | |
# rating not found | |
if verbose == 2: | |
print(t.red("no rating")) | |
print(etree.tostring(song, pretty_print=True).decode()) | |
if verbose: | |
print(t.bold("."), t.italic(filename)) | |
if isinstance(rating, int): | |
# delete old rating tag | |
for old_rating in song.findall("rating"): | |
song.remove(old_rating) | |
# write to database | |
temp = etree.Element("rating") | |
temp.text = str(rating) | |
song.append(temp) | |
bar() | |
# write XML with pretty formatting and correct closing tags | |
tree.write("derp.xml", pretty_print=True, xml_declaration=True, encoding="utf-8") | |
print(t.italic("now overwrite your current database:")) | |
print(f"\nmv derp.xml {DATABASE}\n") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Revision 7 parses 979 files/sec. Maybe I'll convert to switch to

ThreadPoolExecutor
to speed this up later.