Thanks GMWeezel!! I've been wrestling with this one for a long time. I'm ridiculously picky, so I didn't actually end up using your code, but you inspired me to take another shot.
I wanted to be able to automatically back up all of my Rhythmbox playlists to .m3u format (more or less) in a specified folder. I also wanted to be able to specify a subset of those playlists, and have that subset be synced with my 8GB Fuze (I don't use the expansion slot). I couldn't have a local mirror of the Fuze due to there not being enough room on my hard drive. To accommodate these needs, I came up with two Python scripts: One to back up the playlists and one to sync the Fuze. I don't think many people would prefer these over your easier-to-use and more capable script, but here they are just in case.
Please don't set PORTABLE_MUSIC='/', don't use this code in your military satellite navigation software, and in general don't do anything that would make you want to sue me. Public domain.
save_rb_playlists.py
Code:
#!/usr/bin/python3
# This script exports all of the static playlists from Rhythmbox to
# M3U format (without the EXTINF bits) in a given directory.
# Note: The script was designed for Rhythmbox version 1.6. It should
# work for any version of Rhythmbox whose playlists.xml file is
# compatible with version 1.6.
import urllib.parse
import os.path
import xml.dom.minidom
# Location of Rhythmbox's playlists.xml file. We assume Rhythmbox
# version 1.6 (or any version whose playlists.xml file is compatible
# with version 1.6).
RHYTHMBOX_PLAYLISTS = os.path.expanduser('~/.local/share/rhythmbox/'
'playlists.xml')
# Directory where playlists are stored on the local machine.
PLAYLISTS_DIR = os.path.expanduser('~/Documents/playlists/')
def check_node_name(node, name):
if node.nodeName != name:
raise Exception('Expected "%s" node but got "%s".'
% (name, node.nodeName))
def main():
if not PLAYLISTS_DIR.endswith('/'):
raise Exception('PLAYLISTS_DIR must end with a /.')
with open(RHYTHMBOX_PLAYLISTS) as doc:
dom = xml.dom.minidom.parse(doc)
rhythmdb_playlists = dom.firstChild
check_node_name(rhythmdb_playlists, 'rhythmdb-playlists')
for p in rhythmdb_playlists.childNodes:
if (p.nodeType == p.ELEMENT_NODE
and p.attributes['type'].value == 'static'):
check_node_name(p, 'playlist')
# This is an actual static playlist node, not some silly
# text node or automatic/queue playlist node.
playlist_name = p.attributes['name'].value
playlist_fn = os.path.join(PLAYLISTS_DIR,
playlist_name + '.m3u')
print('Putting %s.' % playlist_fn)
song_lines = ['#EXTM3U\n']
for s in p.childNodes:
if s.nodeType == s.ELEMENT_NODE:
check_node_name(s, 'location')
song_uri = s.firstChild.nodeValue.strip()
if song_uri.startswith('file://'):
song_fn = urllib.parse.unquote(song_uri[7:])
if not os.path.isfile(song_fn):
raise Exception('Song "%s" in playlist "%s" '
'does not seem to exist.'
% (song_fn, playlist_name))
song_lines.append(song_fn + '\n')
with open(playlist_fn, 'w') as pl_file:
pl_file.writelines(song_lines)
if __name__ == '__main__':
main()
sync_fuze.py
Code:
#!/usr/bin/python3
# This script syncs a collection of M3U playlists to a portable music
# player (which has MSC mode or can otherwise be accessed like a
# directory in the filesystem). It was designed for the Sansa Fuze,
# firmware version V01.01.15A, so playlists are formatted for that
# particular device.
# Note: The script does not look for changes to a given file (no
# checksums are computed). If you e.g. change the ID3 tag of a song
# on your local machine, you will need to delete the old version on the
# portable player in order for the new version to be synced.
import os.path
import os
import shutil
# Directory where playlists are stored on the local machine.
PLAYLISTS_DIR = os.path.expanduser('~/Documents/playlists/')
# Music directory on the local machine.
LOCAL_MUSIC = os.path.expanduser('~/Music/')
# Music directory on the portable player. Note that .m3u files
# for synced playlists will be placed in this directory.
PORTABLE_MUSIC = '/media/0123-4567/MUSIC/'
# The songs in these playlists will be synced. The playlists
# themselves (the .m3u files) will also be synced.
IMPORT_PLAYLISTS = ('l', 'm', 'n', 't')
# The songs in these playlists will be synced. The playlists
# themselves will NOT be synced.
IMPORT_OTHER = ('o',)
def main():
if not LOCAL_MUSIC.endswith('/'):
raise Exception('LOCAL_MUSIC must end with a /.')
if not PLAYLISTS_DIR.endswith('/'):
raise Exception('PLAYLISTS_DIR must end with a /.')
if not PORTABLE_MUSIC.endswith('/'):
raise Exception('PORTABLE_MUSIC must end with a /.')
playlists = []
# Extract songs from the specified playlists.
for pl in IMPORT_PLAYLISTS + IMPORT_OTHER:
with open(os.path.join(PLAYLISTS_DIR, pl + '.m3u')) as pl_file:
pl_songs = []
for line in pl_file.readlines():
l = line.strip()
if not l.startswith('#'):
if not l.startswith(LOCAL_MUSIC):
raise Exception('Song "%s" in playlist "%s" is not in '
'LOCAL_MUSIC, violating our '
'assumption.' % (l, pl))
if not os.path.isfile(l):
raise Exception('Song "%s" in playlist "%s" does not '
'seem to exist.'
% (l, pl))
pl_songs.append(l[len(LOCAL_MUSIC):])
playlists.append(pl_songs)
# Delete files in the portable music directory that are not in any
# of the playlists.
for root, dirs, files in os.walk(PORTABLE_MUSIC, topdown=False):
for f in files:
f_path = os.path.join(root, f)
f_tail = f_path[len(PORTABLE_MUSIC):]
if True not in ((f_tail in pl_songs) for pl_songs in playlists):
# This file is not in any of the playlists.
print('Removing %s.' % f_path)
os.remove(f_path)
for d in dirs:
d_path = os.path.join(root, d)
if not os.listdir(d_path):
# This directory is empty.
print('Removing %s.' % d_path)
os.rmdir(d_path)
# Add files from the playlists to the portable music directory
# (unless they are already there).
for pl_songs in playlists:
for song in pl_songs:
song_portable = os.path.join(PORTABLE_MUSIC, song)
if not os.path.isfile(song_portable):
# Song isn't already in the portable music directory,
# so put it there.
song_local = os.path.join(LOCAL_MUSIC, song)
song_portable_dir = os.path.dirname(song_portable)
print('Putting %s.' % song_portable)
if not os.path.isdir(song_portable_dir):
os.makedirs(song_portable_dir)
shutil.copy(song_local, song_portable)
# Add the playlists themselves (the .m3u files) to the portable
# music directory.
for i in range(len(IMPORT_PLAYLISTS)):
pl = IMPORT_PLAYLISTS[i]
pl_songs = playlists[i]
pl_portable = os.path.join(PORTABLE_MUSIC, pl + '.m3u')
print('Putting %s.' % pl_portable)
with open(pl_portable, 'w') as pl_file:
# Use relative paths with DOS-like directory separators
# and line ends.
pl_file.write('#EXTM3U\r\n')
pl_file.writelines(song.replace('/', '\\') + '\r\n'
for song in pl_songs)
if __name__ == '__main__':
main()
The scripts use Python 3 because the urllib.unquote in Python 2.6 wasn't doing it for me. I of course welcome any feedback. GMWeezel, thanks again for the inspiration!
Chandler
Bookmarks