Skip to content

Instantly share code, notes, and snippets.

@Teque5
Last active August 4, 2025 21:13
Show Gist options
  • Save Teque5/69d5ece2003e7ea1f3c29daf01b411a2 to your computer and use it in GitHub Desktop.
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…
#!/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")
@Teque5
Copy link
Author

Teque5 commented Jun 5, 2024

Revision 7 parses 979 files/sec. Maybe I'll convert to switch to ThreadPoolExecutor to speed this up later.
2024-06-05_popm2rb

@Teque5
Copy link
Author

Teque5 commented Aug 4, 2025

Revision 12 parses 1373 files/sec. ThreadPoolExecutor and ProcessPoolExecutor were both slower.
popm2rb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment