File: flatten-itunes-2.py

#!/usr/bin/env python3
"""
==========================================================================
flatten-tunes - copy music files in any tree to flat folders.
License: provided freely, but with no warranties of any kind.
Author/copyright: 2011, M. Lutz (http://learning-python.com).

Version 2, Nov-11: Normalize all iTunes tree files into a uniform
4 subfolders per device or location, to better support differences
when iTunes trees have diverged badly.  This also keeps and copies
every file in the iTunes tree: any duplicates are renamed with a
numeric extension to make them unique.  Also updated to run on both
Python 2.X and 3.X.  Run the book's dirdiff.py to compare results.
This version was polished slightly in May 2018 (inputs and docs).

Output appears in the Flattened/* subfolders of the output folder.
Note: this can flatten any folder tree, not just an iTunes tree;
Amazon MP3 zipfile downloads, for example, are similarly nested.
See also rename-itunes.py: strip leading track numbers in filenames.
More details: learning-python.com/books/pp4e-updates.html#flatten.

Original docs: Flatten iTunes subfolder tree contents to store on
a USB drive, for copying onto devices where subdirectories don't
work well (e.g., a vehicle's harddrive).  Code note: str.endswith() 
now allows a tuple of strings to try, so we don't need to say
any(filelower.endswith(x) for x in seq).

==========================================================================
Example run (Mac OS):

/MY-STUFF/Code$ python3 flatten-itunes-2.py 
Input folder?  (e.g., /Users/you/Music) /Users/mini/Desktop/test/new-may-2018
Output folder? (e.g., /Volumes/SSD/resolve-itunes) /Users/mini/Desktop/test             

/MY-STUFF/Code$ ls /Users/Mini/Desktop/test
Flattened Flattened-results.txt new-may-2018

/MY-STUFF/Code$ ls /Users/Mini/Desktop/test/Flattened
Irrelevant Other Playable Protected

/MY-STUFF/Code$ ls /Users/Mini/Desktop/test/Flattened/Playable
01 - Brave New World.mp3
01 - Easy Come Easy Go.mp3
...

/MY-STUFF/Code$ python3 rename-itunes.py 
Folder to scan? /Users/mini/Desktop/test/Flattened/Playable

Rename? "02 - Pacific Coast Highway.mp3" y
new name? ["Pacific Coast Highway.mp3"] 
renamed: "02 - Pacific Coast Highway.mp3" -> "Pacific Coast Highway.mp3"
...
==========================================================================
"""

from __future__ import print_function  # to run on python 2.X

import os, pprint, sys
if sys.version_info[0] == 2: input = raw_input  # more 2.X compat

#-------------------------------------------------------------------------
# Get from/to dirs at console: absolute or relative paths
#-------------------------------------------------------------------------

if sys.platform.startswith('win'):
    exfrom, exto = r'C:\Users\you\Itunes', r'F:\resolve-itunes'
else:
    exfrom, exto = '/Users/you/Music', '/Volumes/SSD/resolve-itunes'

itunesRoot = input('Input folder?  (e.g., %s) ' % exfrom)   # music tree root
outputRoot = input('Output folder? (e.g., %s) ' % exto)     # 4 output folders here
outputDir  = os.path.join(outputRoot,  'Flattened')

# existing/valid dirs?
if any(not os.path.isdir(path) for path in (itunesRoot, outputRoot)):
    print('Input or output folder paths are invalid: run cancelled.')
    sys.exit(1)

#-------------------------------------------------------------------------
# Initialize file groupings (or use mimetypes?)
#-------------------------------------------------------------------------

categories = [
    dict(name='Playable',   exts=('.mp3', '.m4a')),
    dict(name='Protected',  exts=('.m4p',)),
    dict(name='Irrelevant', exts=('.jpg', '.ini', '.xml')),
    dict(name='Other',      exts=None) ]

for cat in categories:
     cat['subdir']     = os.path.join(outputDir, cat['name'])
     cat['members']    = []
     cat['duplicates'] = 0
     cat['dupnames']   = []

#-------------------------------------------------------------------------
# Make copy-tree dirs if needed
#-------------------------------------------------------------------------

for dirpath in [outputDir] + [cat['subdir'] for cat in categories]:
    if not os.path.exists(dirpath):
        os.mkdir(dirpath)

#-------------------------------------------------------------------------
# Copy one file to flat dir
#-------------------------------------------------------------------------

def noteAndCopy(category, dirfrom, filename):
    """
    copy one file from itunes/music tree to normalized tree;
    rename file with a numeric suffix is it's a duplicate,
    so don't overwrite prior file (e.g. "track-N" generic
    names for tracks in different subdirs);  note the "while":
    though unlikely, the renamed name may also be a duplicate!
    also note the simple copy: read/write in chunks if needed;
    TBD - dup numbers per filename instead of per category?
    """
    fromname = toname = filename
    if toname in category['members']:
        category['duplicates'] += 1
        basename, ext = os.path.splitext(toname)
        suffix = '--%s' % category['duplicates']
        toname = basename + suffix + ext
        while toname in category['members']:
            suffix += '_'
            toname = basename + suffix + ext
        category['dupnames'].append(toname)

    category['members'].append(toname)  # plus dir?
    copyfrom = os.path.join(dirfrom, fromname)
    copyto   = os.path.join(category['subdir'], toname)
    open(copyto, 'wb').write( open(copyfrom, 'rb').read() )

#-------------------------------------------------------------------------
# Walk the itunes/music tree
#-------------------------------------------------------------------------

for (dirHere, subsHere, filesHere) in os.walk(itunesRoot):
    for file in filesHere:
        for cat in categories[:-1]:
            if file.lower().endswith(cat['exts']):
                noteAndCopy(cat, dirHere, file)                
                break
        else:
            other = categories[-1]
            noteAndCopy(other, dirHere, file)

#-------------------------------------------------------------------------
# Report results
#-------------------------------------------------------------------------

os.environ['PYTHONIOENCODING'] = 'utf-8'                      # filenames? too late?
resultsFile = outputRoot + os.sep + 'Flattened-results.txt'   # same dir as outputs
sys.stdout  = open(resultsFile, 'w')                          # send prints to a file 

for category in categories:
     print('*' * 80)
     for key in ('name', 'exts', 'subdir', 'duplicates', 'members', 'dupnames'):
         print(key, '=>', end=' ')
         if key in ('members', 'dupnames'): print()
         pprint.pprint(category[key])

print('=' * 80)
for cat in categories:
    print('Total %s: %s' % (cat['name'].ljust(10), len(cat['members'])))



[Home] Books Programs Blog Python Author Training Search Email ©M.Lutz