Python Script To Copy a Playlist and Linked Files

Most tools for copying files onto MP3 players (often actually a phone these days) work on the basis that you copy your entire music catalogue and then make playlists linking to various files on it.

It can be a trickier process to copy your favourite selected audio files – the ones specifically used in your playlist, that usually exist in a deep structure of their own. There seem to be some proprietary tools for it, but here’s a Free (in all senses) and Open Source solution.

I adapted my previous Python scripts (that allow for files to be appended to a playlist on the command line, and shuffle a playlist from the command line) to make a – well – bit of a Frankenstein’s monster that will take a playlist as input, and copy the playlist and all its referenced file to another directory. It will create the required directory structure, but will only copy the actual music files on the playlist.

I have used this successfully to move a playlist and the selected directory structure onto more modest sized music players.

Usage looks something like this. -v -i /path/to/your/list.m3u -o /path/where/you/want/it/all.m3u

Have fun, use with care, and I’ll try to clean it up at some stage. If you break something, you own all the pieces, but feel free to write me a (nice) comment below.

At some point I’ll move these scripts to Python 3 and off the deprecated option handling code module.

#!/usr/bin/env python

# Simple script to copy a whole playlist and its contents
# Colin Turner <>
# 2016
# GPL v2
# Need to move all of these scripts to Python 3 in due course

import random
import re
import os
import shutil
import eyeD3

# We want to be able to process some command line options.
from optparse import OptionParser

def process_lines(options, all_lines):
  'process the list of all playlist lines into three chunks'
  # Eventually we want to support several formats
  m3u = True
  extm3u = False
  if options.verbose:
    print "Read %u lines..." % len(all_lines)
  header = list()
  middle = list()
  footer = list()

  # Check first line for #EXTM3U
  if re.match("^#EXTM3U", all_lines[0]):     if options.verbose:       print "EXTM3U format file..."     extm3u = True     header.append(all_lines[0])     del all_lines[0]    loop = 0   while loop < len(all_lines):     # Each 'item' may be multiline     item = list()     if re.match("^#EXTINF.*", all_lines[loop]):
      loop = loop + 1
    # A proper regexp for filenames would be good
    if loop < len(all_lines):
      loop = loop + 1
    if options.verbose: print item

  return (header, middle, footer)

def load_playlist(options):
  'loads the playlist into an array of arrays'
  if options.verbose:
    print "Reading playlist %s ..." % options.in_filename
  with open(options.in_filename, 'r') as file:
    all_lines = file.readlines()
  (header, middle, footer) = process_lines(options, all_lines)
  return (header, middle, footer)

def write_playlist(options, header, middle, footer):
  'writes the shuffled playlist'
  if options.verbose:
    print "Writing playlist %s ..." % options.out_filename
  with open(options.out_filename, 'w') as file:
    for line in header:
    for item in middle:
        # Get the filename, as it will be
        mp3_filename = resolve_mp3_filename(item[1], options)
        copy_mp3(item[1], mp3_filename, options)
        for line in item:
          # TODO We should wewrite line 1
    for line in footer:

def copy_mp3(source, destination, options):
  'copys an individual mp3 file, making directories as needed'
  source_playlist_path = os.path.dirname(os.path.abspath(options.in_filename))

  destination_playlist_path = os.path.dirname(os.path.abspath(options.out_filename))

  mp3_source = source_playlist_path + os.path.sep + source
  mp3_destination = destination_playlist_path + os.path.sep + destination

  mp3_destination_dir = os.path.dirname(os.path.abspath(mp3_destination))

  if not os.path.exists(mp3_destination_dir.rstrip()):

  shutil.copyfile(mp3_source.rstrip(), mp3_destination.rstrip())

def copy(options):
  'perform the copy on the playlist'
  # read the existing data into three arrays in a tuple
  (header, middle, footer) = load_playlist(options)
  # and shuffle the lines array
  if options.verbose:
    print "Copying..."
  # now spit them back out
  write_playlist(options, header, middle, footer)

def append(options, artist, title, seconds):
  'append the fetched data to the playlist'
  mp3_filename = resolve_mp3_filename(options)
  # Check if the playlist file exists
  there_is_no_spoon = not os.path.isfile(options.out_filename)

  with open(options.out_filename, 'a+') as playlist:
    # was the file frshly created?
    if there_is_no_spoon:
      # So write the header
      print >> playlist, "#EXTM3U"
      # There was a file, so check the last character, in case there was no \n, os.SEEK_END)
      last_char =
      if(last_char != '\n'):
        print >> playlist

    # OK, now able to write
    print >> playlist, "#EXTINF:%u,%s - %s" % (seconds, artist, title)
    print >> playlist, "%s" % mp3_filename

def resolve_mp3_filename(filename, options):
  'resolve the mp3 filename appropriately, if we can, and if we are asked to'
  'there are three modes, depending on command line parameters:'
  '-l we write the filename precisely as on the command list'
  '-r specifies a base relative which to write the filename'
  'otherwise we try to resolve relative to the directory of the playlist'
  'the absolute filename will be the fall back position is resolution is impossible'

  if options.leave_filename:
    # we have been specifcally told not to resolve the filename
    mp3_filename = filename
    if options.verbose:
      print "Filename resolution disabled."

  if not options.leave_filename and not len(options.relative_to):
    # Neither argument used, automatcally resolve relative to the playlist
    (playlist_path, playlist_name) = os.path.split(os.path.abspath(options.out_filename))
    options.relative_to = playlist_path + os.path.sep
    if options.verbose:
      print "Automatic filename resolution relative to playlist base %s" % options.relative_to

  if len(options.relative_to):
    # We have been told to map the path relative to another path
    mp3_filename = os.path.abspath(filename)
    # Check that the root is actually present
    if mp3_filename.find(options.relative_to) == 0:
      # It is present and at the start of the line
      mp3_filename = mp3_filename.replace(options.relative_to, '', 1)

  if options.verbose:
    print "mp3 filename will be written as %s..." % mp3_filename
  return mp3_filename

def get_meta_data(options):
  'perform the append on the playlist'
  # read the existing data into three arrays in a tuple
  if options.verbose:
    print "Opening MP3 file %s ..." % options.in_filename
  if eyeD3.isMp3File(options.in_filename):
    # Ok, so it's an mp3
    audioFile = eyeD3.Mp3AudioFile(options.in_filename)
    tag = audioFile.getTag()
    artist = tag.getArtist()
    title = tag.getTitle()
    seconds = audioFile.getPlayTime()
    if not options.quiet:
      print "%s - %s (%s s)" % (artist, title, seconds)
    # OK, we have the required information, now time to write to the playlist
    return artist, title, seconds
    print "Not a valid mp3 file."

def print_banner():
  print "playlist-copy"

def main():
  'the main function that kicks everything else off'

  usage = "usage: %prog [options] arg"
  parser = OptionParser(usage)
  parser.add_option("-i", "--input-file", dest="in_filename",
                    help="read playlist from FILENAME")
  parser.add_option("-o", "--output-file", dest="out_filename",
                    help="write new playlist to FILENAME")
  parser.add_option("-l", "--leave-filename", dest="leave_filename", action="store_false",
                    help="leaves the mp3 path as specified on the command line, rather than resolving it")
  parser.add_option("-r", "--relative-to", dest="relative_to", default="",
                    help="resolves mp3 filename relative to this path")
  parser.add_option("-v", "--verbose",
                    action="store_true", dest="verbose")
  parser.add_option("-q", "--quiet", default=False,
                    action="store_true", dest="quiet")

  (options, args) = parser.parse_args()
#  if len(args) == 0:
#      parser.error("use -h for more help")

  if not options.quiet:


  if not options.quiet:
      print "Playlist copy complete..."

if  __name__ == '__main__':


  • 4

Leave a Reply

Your email address will not be published. Required fields are marked *