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.
1 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
#!/usr/bin/env python # # Simple script to copy a whole playlist and its contents # Colin Turner <ct@piglets.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[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]): 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[1], options) copy_mp3(item[1], 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() |