Saturday, 19 June 2010

Synchronize your Foobar2000 playlists with your Android device (or any other MP3 player)

I recently decided I wanted to synchronize some of my music with my Nexus One. I quickly discovered that the Nexus One didn't support WMA audio files. I also discovered there weren't any out-of-the-box solutions for syncing audio files and playlists with the flexibility I required.

So I decided to write my own between two World Cup matches. I felt the script might be useful to other people and I couldn't find a suitable place to put it. So I am dumping it here.

You will need the following software installed to use this script.

Foobar2000
Com Automation Server for Foobar2000
Python 2.6.*
Win32 Extensions for Python
FFmpeg

All my music files are currently in WMA format. This will change in the future but until then the script converts the files into MP3 using FFmpeg. This can be changed if you need by adjusting the function "convert_file".

Copy and paste the code below into a file called syncplayer.py. You may need to edit some of the settings at the top of the file. If you have Python files correctly associated on your PC then you should be able to doubleclick the file and the sync will run whilst displaying a log on the console.

Enjoy!


"""
SyncPlayer.py - A Python script for synchronizing music files between
Foobar2000 and an MP3 device.

Copyright (C) 2010 Blair Sutton

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""

########################
#### SETTINGS BELOW ####
########################

# this is an array of foobar2000 playlist names you want synched
#
playlists = [ "NexusOne", "Driving Music" ]

# this is the path to your android/mp3 player music folder once mounted.
# the converted files will be placed here.
#
destination_root = r"f:\Music"

# this is the path to your android/mp3 player playlist folder. m3u files will
# place here.
#
playlist_root = r"f:\Music\Playlists"

# this is your target conversion format.
#
destination_ext = ".mp3"

# this is how many path levels including your source audio file are synced to the
# device. i.e. artist/album/audio file
#
path_depth = 3

# change these paths to reflect where your converters are installed
#
ffmpeg_path = r"C:\Program Files\FFmpeg-0.6-svn-23607\bin\ffmpeg.exe"

####################
#### CODE BELOW ####
####################

tag_list = ["Author","Title","UserRating","UserServiceRating","WM/AlbumArtist","WM/AlbumTitle",
"WM/Category","WM/Composer","WM/Conductor","WM/ContentDistributor","WM/ContentGroupDescription",
"WM/EncodingTime","WM/Genre","WM/GenreID","WM/InitialKey","WM/Language","WM/Lyrics","WM/MCDI",
"WM/MediaClassPrimaryID","WM/MediaClassSecondaryID","WM/Mood","WM/ParentalRating","WM/Period",
"WM/ProtectionType","WM/Provider","WM/ProviderRating","WM/ProviderStyle","WM/Publisher",
"WM/SubscriptionContentID","WM/SubTitle","WM/TrackNumber","WM/UniqueFileIdentifier",
"WM/WMCollectionGroupID","WM/WMCollectionID","WM/WMContentID","WM/Writer","WM/Year"]

from win32com.client.gencache import EnsureDispatch
from os.path import basename, splitext, exists
from os import sep, makedirs
from subprocess import call
from urlparse import urlparse
from logging import StreamHandler, Formatter, DEBUG, INFO, getLogger

log = getLogger("Foobar.MP3PlayerSync")
log.setLevel(DEBUG)
lh = StreamHandler()
lh.setFormatter(Formatter("%(levelname)s|%(asctime)s|%(filename)s:%(lineno)d|%(message)s"))
log.addHandler(lh)

log.info("Connecting to foobar2000 COM server...")

prog_id = "Foobar2000.Application.0.7"
fb2k = EnsureDispatch(prog_id)

def main():
fb2k_playlists = [ i for i in fb2k.Playlists if i.Name in playlists ]
if fb2k_playlists:
for pl in fb2k_playlists:
sync_playlist(pl)
log.info("Completed Sync")

def sync_playlist(sync_playlist):
log.info("Syncing playlist '%s'..." % sync_playlist.Name)
tracks = sync_playlist.GetTracks()

m3u_lines = ["#EXTM3U"]
for t in tracks:
m3u_lines.append(t.FormatTitle("#EXTINF:%length_seconds%, %artist% - %title%"))
source_path = urlparse(t.Path).netloc
m3u_lines.append(sync_file(source_path))

create_m3u(sync_playlist.Name, m3u_lines)

def sync_file(source_path):
parts_all = source_path.split(sep)
parts = parts_all[-path_depth:]

filenameext = parts[path_depth-1]
(filename, ext) = splitext(filenameext)

parts_new_path = [destination_root]
parts_new_path.extend(parts[0:path_depth-1])
destination_folder = sep.join(parts_new_path)
parts_new_path.append(filename + destination_ext)
destination_path = sep.join(parts_new_path)

if not exists(destination_folder):
log.info("Creating folder: '%s'..." % destination_folder)
makedirs(destination_folder)

if not exists(destination_path):
convert_file(source_path, destination_path)

return destination_path

# For my purposes I needed to convert my WMA files (without DRM!) to MP3 so
# my Nexus could understand them. You could skip this bit if your source and
# destination codecs are the same on your devices.
#
def convert_file(input_file, output_file):
log.info("Synching: '%s' -> '%s'" % (input_file, output_file))
command = """"%s" -i "%s" -ac 2 -ar 22050 -ab 192k "%s" """ % (ffmpeg_path, input_file, output_file)
log.debug("Converter command line: '%s'" % command)
try:
retcode = call(command, shell=False)
except OSError, e:
log.critical("Converter execution failed: ", e)

log.info("Copying media tags over..")
wmp = EnsureDispatch("WMPlayer.OCX")
if_m = wmp.newMedia(input_file)
of_m = wmp.newMedia(output_file)
for tag in tag_list:
if not of_m.isReadOnlyItem(tag) and if_m.getItemInfo(tag) != "":
of_m.setItemInfo(tag, if_m.getItemInfo(tag))


def create_m3u(playlist_name, m3u_lines):
if not exists(playlist_root):
log.info("Creating folder: '%s'..." % playlist_root)
makedirs(playlist_root)

m3u_path = "%s\\%s.m3u" % (playlist_root, playlist_name)
log.info("Creating m3u playlist: '%s'..." % m3u_path)
f = open(m3u_path, "w+")
f.write("\n".join(m3u_lines))
f.close()


if __name__ == "__main__":
main()