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.
playlist-copy.py -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 <email@example.com> # 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[loop]): item.append(all_lines[loop]) loop = loop + 1 # A proper regexp for filenames would be good if loop < len(all_lines): item.append(all_lines[loop]) loop = loop + 1 if options.verbose: print item middle.append(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: file.write(line) for item in middle: # Get the filename, as it will be mp3_filename = resolve_mp3_filename(item, options) copy_mp3(item, mp3_filename, options) for line in item: # TODO We should wewrite line 1 file.write(line) for line in footer: file.write(line) 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()): os.makedirs(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" else: # There was a file, so check the last character, in case there was no \n playlist.seek(-1, os.SEEK_END) last_char = playlist.read(1) 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 else: print "Not a valid mp3 file." exit(1) 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: print_banner() copy(options) if not options.quiet: print "Playlist copy complete..." if __name__ == '__main__': main()