{"id":549,"date":"2016-08-29T15:18:31","date_gmt":"2016-08-29T15:18:31","guid":{"rendered":"http:\/\/www.piglets.org\/blog\/?p=549"},"modified":"2016-08-29T15:24:12","modified_gmt":"2016-08-29T15:24:12","slug":"python-script-to-copy-a-playlist-and-linked-files","status":"publish","type":"post","link":"https:\/\/www.piglets.org\/blog\/2016\/08\/29\/python-script-to-copy-a-playlist-and-linked-files\/","title":{"rendered":"Python Script To Copy a Playlist and Linked Files"},"content":{"rendered":"<p>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.<\/p>\n<p>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.<\/p>\n<p>I adapted my previous Python scripts (<a href=\"http:\/\/www.piglets.org\/blog\/2014\/07\/31\/python-script-to-add-a-file-to-a-playlist\/\">that allow for files to be appended to a playlist on the command line<\/a>, and <a href=\"http:\/\/www.piglets.org\/blog\/2014\/07\/31\/python-script-to-randomise-an-m3u-playlist\/\">shuffle a playlist from the command line<\/a>) 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.<\/p>\n<p>I have used this successfully to move a playlist and the selected directory structure onto more modest sized music players.<\/p>\n<p>Usage looks something like this.<\/p>\n<pre class=\"lang:sh decode:true \" title=\"Typical Usage\">playlist-copy.py -v -i \/path\/to\/your\/list.m3u -o \/path\/where\/you\/want\/it\/all.m3u<\/pre>\n<p>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.<\/p>\n<p>At some point I'll move these scripts to Python 3 and off the deprecated option handling code module.<\/p>\n<pre class=\"lang:python decode:true \" title=\"Python Script to Copy a Playlist and Linked Files\">#!\/usr\/bin\/env python\r\n\r\n#\r\n# Simple script to copy a whole playlist and its contents\r\n# Colin Turner &lt;ct@piglets.com&gt;\r\n# 2016\r\n# GPL v2\r\n#\r\n# Need to move all of these scripts to Python 3 in due course\r\n\r\nimport random\r\nimport re\r\nimport os\r\nimport shutil\r\nimport eyeD3\r\n\r\n# We want to be able to process some command line options.\r\nfrom optparse import OptionParser\r\n\r\ndef process_lines(options, all_lines):\r\n  'process the list of all playlist lines into three chunks'\r\n  # Eventually we want to support several formats\r\n  m3u = True\r\n  extm3u = False\r\n  if options.verbose:\r\n    print \"Read %u lines...\" % len(all_lines)\r\n  header = list()\r\n  middle = list()\r\n  footer = list()\r\n\r\n  # Check first line for #EXTM3U\r\n  if re.match(\"^#EXTM3U<img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.piglets.org\/blog\/wp-content\/ql-cache\/quicklatex.com-5771d6f144d2929882974b20349ff1a8_l3.png\" class=\"ql-img-inline-formula quicklatex-auto-format\" alt=\"&#34;&#44;&#32;&#97;&#108;&#108;&#95;&#108;&#105;&#110;&#101;&#115;&#091;&#48;&#093;&#41;&#58; &#32;&#32;&#32;&#32;&#105;&#102;&#32;&#111;&#112;&#116;&#105;&#111;&#110;&#115;&#46;&#118;&#101;&#114;&#98;&#111;&#115;&#101;&#58; &#32;&#32;&#32;&#32;&#32;&#32;&#112;&#114;&#105;&#110;&#116;&#32;&#34;&#69;&#88;&#84;&#77;&#51;&#85;&#32;&#102;&#111;&#114;&#109;&#97;&#116;&#32;&#102;&#105;&#108;&#101;&#46;&#46;&#46;&#34; &#32;&#32;&#32;&#32;&#101;&#120;&#116;&#109;&#51;&#117;&#32;&#61;&#32;&#84;&#114;&#117;&#101; &#32;&#32;&#32;&#32;&#104;&#101;&#97;&#100;&#101;&#114;&#46;&#97;&#112;&#112;&#101;&#110;&#100;&#40;&#97;&#108;&#108;&#95;&#108;&#105;&#110;&#101;&#115;&#091;&#48;&#093;&#41; &#32;&#32;&#32;&#32;&#100;&#101;&#108;&#32;&#97;&#108;&#108;&#95;&#108;&#105;&#110;&#101;&#115;&#091;&#48;&#093;  &#32;&#32;&#108;&#111;&#111;&#112;&#32;&#61;&#32;&#48; &#32;&#32;&#119;&#104;&#105;&#108;&#101;&#32;&#108;&#111;&#111;&#112;&#32;&#60;&#32;&#108;&#101;&#110;&#40;&#97;&#108;&#108;&#95;&#108;&#105;&#110;&#101;&#115;&#41;&#58; &#32;&#32;&#32;&#32;&#35;&#32;&#69;&#97;&#99;&#104;&#32;&#39;&#105;&#116;&#101;&#109;&#39;&#32;&#109;&#97;&#121;&#32;&#98;&#101;&#32;&#109;&#117;&#108;&#116;&#105;&#108;&#105;&#110;&#101; &#32;&#32;&#32;&#32;&#105;&#116;&#101;&#109;&#32;&#61;&#32;&#108;&#105;&#115;&#116;&#40;&#41; &#32;&#32;&#32;&#32;&#105;&#102;&#32;&#114;&#101;&#46;&#109;&#97;&#116;&#99;&#104;&#40;&#34;&#94;&#35;&#69;&#88;&#84;&#73;&#78;&#70;&#46;&#42;\" title=\"Rendered by QuickLaTeX.com\" height=\"84\" width=\"593\" style=\"vertical-align: -4px;\"\/>\", all_lines[loop]):\r\n      item.append(all_lines[loop])\r\n      loop = loop + 1\r\n    # A proper regexp for filenames would be good\r\n    if loop &lt; len(all_lines):\r\n      item.append(all_lines[loop])\r\n      loop = loop + 1\r\n    if options.verbose: print item\r\n    middle.append(item)\r\n\r\n  return (header, middle, footer)\r\n\r\n\r\n\r\ndef load_playlist(options):\r\n  'loads the playlist into an array of arrays'\r\n  if options.verbose:\r\n    print \"Reading playlist %s ...\" % options.in_filename\r\n  with open(options.in_filename, 'r') as file:\r\n    all_lines = file.readlines()\r\n  (header, middle, footer) = process_lines(options, all_lines)\r\n  return (header, middle, footer)\r\n\r\ndef write_playlist(options, header, middle, footer):\r\n  'writes the shuffled playlist'\r\n  if options.verbose:\r\n    print \"Writing playlist %s ...\" % options.out_filename\r\n  with open(options.out_filename, 'w') as file:\r\n    for line in header:\r\n      file.write(line)\r\n    for item in middle:\r\n        # Get the filename, as it will be\r\n        mp3_filename = resolve_mp3_filename(item[1], options)\r\n        copy_mp3(item[1], mp3_filename, options)\r\n        for line in item:\r\n          # TODO We should wewrite line 1\r\n          file.write(line)\r\n    for line in footer:\r\n      file.write(line)\r\n\r\ndef copy_mp3(source, destination, options):\r\n  'copys an individual mp3 file, making directories as needed'\r\n  source_playlist_path = os.path.dirname(os.path.abspath(options.in_filename))\r\n\r\n  destination_playlist_path = os.path.dirname(os.path.abspath(options.out_filename))\r\n\r\n  mp3_source = source_playlist_path + os.path.sep + source\r\n  mp3_destination = destination_playlist_path + os.path.sep + destination\r\n\r\n  mp3_destination_dir = os.path.dirname(os.path.abspath(mp3_destination))\r\n\r\n  if not os.path.exists(mp3_destination_dir.rstrip()):\r\n      os.makedirs(mp3_destination_dir.rstrip())\r\n\r\n  shutil.copyfile(mp3_source.rstrip(), mp3_destination.rstrip())\r\n\r\ndef copy(options):\r\n  'perform the copy on the playlist'\r\n  # read the existing data into three arrays in a tuple\r\n  (header, middle, footer) = load_playlist(options)\r\n  # and shuffle the lines array\r\n  if options.verbose:\r\n    print \"Copying...\"\r\n  # now spit them back out\r\n  write_playlist(options, header, middle, footer)\r\n\r\n\r\ndef append(options, artist, title, seconds):\r\n  'append the fetched data to the playlist'\r\n  mp3_filename = resolve_mp3_filename(options)\r\n  # Check if the playlist file exists\r\n  there_is_no_spoon = not os.path.isfile(options.out_filename)\r\n\r\n  with open(options.out_filename, 'a+') as playlist:\r\n    # was the file frshly created?\r\n    if there_is_no_spoon:\r\n      # So write the header\r\n      print &gt;&gt; playlist, \"#EXTM3U\"\r\n    else:\r\n      # There was a file, so check the last character, in case there was no \\n\r\n      playlist.seek(-1, os.SEEK_END)\r\n      last_char = playlist.read(1)\r\n      if(last_char != '\\n'):\r\n        print &gt;&gt; playlist\r\n\r\n    # OK, now able to write\r\n    print &gt;&gt; playlist, \"#EXTINF:%u,%s - %s\" % (seconds, artist, title)\r\n    print &gt;&gt; playlist, \"%s\" % mp3_filename\r\n\r\n\r\ndef resolve_mp3_filename(filename, options):\r\n  'resolve the mp3 filename appropriately, if we can, and if we are asked to'\r\n  'there are three modes, depending on command line parameters:'\r\n  '-l we write the filename precisely as on the command list'\r\n  '-r specifies a base relative which to write the filename'\r\n  'otherwise we try to resolve relative to the directory of the playlist'\r\n  'the absolute filename will be the fall back position is resolution is impossible'\r\n\r\n  if options.leave_filename:\r\n    # we have been specifcally told not to resolve the filename\r\n    mp3_filename = filename\r\n    if options.verbose:\r\n      print \"Filename resolution disabled.\"\r\n\r\n  if not options.leave_filename and not len(options.relative_to):\r\n    # Neither argument used, automatcally resolve relative to the playlist\r\n    (playlist_path, playlist_name) = os.path.split(os.path.abspath(options.out_filename))\r\n    options.relative_to = playlist_path + os.path.sep\r\n    if options.verbose:\r\n      print \"Automatic filename resolution relative to playlist base %s\" % options.relative_to\r\n\r\n  if len(options.relative_to):\r\n    # We have been told to map the path relative to another path\r\n    mp3_filename = os.path.abspath(filename)\r\n    # Check that the root is actually present\r\n    if mp3_filename.find(options.relative_to) == 0:\r\n      # It is present and at the start of the line\r\n      mp3_filename = mp3_filename.replace(options.relative_to, '', 1)\r\n\r\n  if options.verbose:\r\n    print \"mp3 filename will be written as %s...\" % mp3_filename\r\n  return mp3_filename\r\n\r\ndef get_meta_data(options):\r\n  'perform the append on the playlist'\r\n  # read the existing data into three arrays in a tuple\r\n  if options.verbose:\r\n    print \"Opening MP3 file %s ...\" % options.in_filename\r\n  if eyeD3.isMp3File(options.in_filename):\r\n    # Ok, so it's an mp3\r\n    audioFile = eyeD3.Mp3AudioFile(options.in_filename)\r\n    tag = audioFile.getTag()\r\n    artist = tag.getArtist()\r\n    title = tag.getTitle()\r\n    seconds = audioFile.getPlayTime()\r\n    if not options.quiet:\r\n      print \"%s - %s (%s s)\" % (artist, title, seconds)\r\n    # OK, we have the required information, now time to write to the playlist\r\n    return artist, title, seconds\r\n  else:\r\n    print \"Not a valid mp3 file.\"\r\n    exit(1)\r\n\r\n\r\ndef print_banner():\r\n  print \"playlist-copy\"\r\n\r\ndef main():\r\n  'the main function that kicks everything else off'\r\n\r\n  usage = \"usage: %prog [options] arg\"\r\n  parser = OptionParser(usage)\r\n  parser.add_option(\"-i\", \"--input-file\", dest=\"in_filename\",\r\n                    help=\"read playlist from FILENAME\")\r\n  parser.add_option(\"-o\", \"--output-file\", dest=\"out_filename\",\r\n                    help=\"write new playlist to FILENAME\")\r\n  parser.add_option(\"-l\", \"--leave-filename\", dest=\"leave_filename\", action=\"store_false\",\r\n                    help=\"leaves the mp3 path as specified on the command line, rather than resolving it\")\r\n  parser.add_option(\"-r\", \"--relative-to\", dest=\"relative_to\", default=\"\",\r\n                    help=\"resolves mp3 filename relative to this path\")\r\n  parser.add_option(\"-v\", \"--verbose\",\r\n                    action=\"store_true\", dest=\"verbose\")\r\n  parser.add_option(\"-q\", \"--quiet\", default=False,\r\n                    action=\"store_true\", dest=\"quiet\")\r\n\r\n  (options, args) = parser.parse_args()\r\n#  if len(args) == 0:\r\n#      parser.error(\"use -h for more help\")\r\n\r\n  if not options.quiet:\r\n    print_banner()\r\n\r\n  copy(options)\r\n\r\n  if not options.quiet:\r\n      print \"Playlist copy complete...\"\r\n\r\n\r\nif  __name__ == '__main__':\r\n  main()\r\n\r\n\r\n<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"vkexunit_cta_each_option":"","footnotes":"","jetpack_publicize_message":"Python Script To Copy a Playlist and Linked Files","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2}},"categories":[10],"tags":[],"class_list":["post-549","post","type-post","status-publish","format-standard","hentry","category-15-random-musings"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack-related-posts":[],"jetpack_shortlink":"https:\/\/wp.me\/p52I4w-8R","_links":{"self":[{"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/posts\/549","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/comments?post=549"}],"version-history":[{"count":2,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/posts\/549\/revisions"}],"predecessor-version":[{"id":551,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/posts\/549\/revisions\/551"}],"wp:attachment":[{"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/media?parent=549"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/categories?post=549"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.piglets.org\/blog\/wp-json\/wp\/v2\/tags?post=549"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}