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()

3 comments:

Jon said...

Hi, I find myself in the same situation - using Windows 7, foobar2000 and wanting to sync music (via smart playlists) to my shiny new Nexus One.

Quite amazed that such a thing doesn't exist yet so I'll give your script a go...

Cheers!

Josh said...

As a complete python noob, I've spent the last few days adapting this script to my specific needs. The code is no doubt a mess, but it does work for me, so will paste back here to help anyone else who may find this useful.

http://pastebin.com/9aseWXSQ

Requirements will be same as above with a few additional things such as neroaacenc etc. all should be specified in the global variables.

Convert command used to process images is part of ImageMagick (http://www.google.co.nz/url?sa=t&rct=j&q=imagemagik&source=web&cd=1&ved=0CDQQFjAA&url=http%3A%2F%2Fwww.imagemagick.org%2F&ei=sf8LT-6bN8m1iQeQsYzzBQ&usg=AFQjCNEj86ZXrN6Iqx9rZ73Ad4L4ypmDtQ&sig2=5Jlv2oYmHzmw-iroj2nlrg) which should be in your path if installed correctly.

The specific changes I made were:

Changed script to take no action when expected files already existed. Also will create output filenames based on artist and track metadata. Not tested with path_depth set to anything other than 1.

Changed ffmpeg command to pipe through to neroaac to make m4a files rather than mp3s.

From artist and album info download album art with AlbumArtDownloader automatically if albumart file does not exist already. Use convert command to strip EXIF data and re-size to max resolution of 720px (Galaxy Nexus).

Found WMPlayer.OCX was unreliable to write metadata to m4a files, so using neroaactag to set metadata and albumart data in m4a file.

Obviously specific parameters can be edited in the commands called e.g -q6 can be lowered in neroaac command if lower bitrates are desired.

All provided as is where is, but is useful for me.

dvrs said...

This is awesome. After 6 years I wanted to get back into programming and choose Python. And it just so happens that I recently got an Android device, so I was looking for exactly this. Seems like a good way to start :)