Multi-module demo: Itunes Liberator

Demo modules: TinyTag, os, sys, sqlite3, pathlib (import Path), shutil

6/8/21 – Posted new version with bug corrections

There are a ton of different reasons why you might want to get all your iTunes out of that maze of subterranean subfolders Apple puts them in – assuming to haven’t already sold your soul to Pandora or Amazon or Spotify.  Maybe you want to create a mega CD for your truck, a superfast server with a Raspberry Pi, a reference for the family to stream from your NAS – whatever.  Maybe you want to get rid of all the numbers (like “01 – My song”)  forcing the tunes into album order.  Maybe you want to add the artist at the end in some way you that would make a standard windows search easy. We do all of that here.

The key is a small module available on PyPy called TinyTag that will deliver unto you all the standard information recorded in an mp3 song header. You will find it here:
tinytag 1.5.0
Install with: pip install tinytag

This little program does all that while being a demo source for all the modules listed above.


# iTunes Liberator - reclaim your mp3's

# V060721     corrections:
#      corrections to support ripped vinal that may not have all header information
#      error check to make sure source and dest are good folders
#      allow no artist by substituting 'unavailable'
#      allow no title by substituting file name
#      use os.path.join to concatenate folder and track name together

# put from_folder and to_folder in variable def area below
# This will create a new name from the song by taking the title
# information from the header, cleaning it up, adding " ~ ", then
# adding the artist name.  So instead of "01 - All Shook Up"  you
# will get "All Shook Up ~ Elvis Presley" as a title

from tinytag import TinyTag, TinyTagException
import os
import sys
import sqlite3 as sq
from sqlite3 import Error
from pathlib import Path as p
import shutil

#  Establish variables/path information - sub your own folder paths, of course

from_folder = p(r"M:\Valeries ipod selection Dec 2009")   # <- Put the master source of music folder here
to_folder = p(r"M:\Temp")         # <- Put the destination folder holding all songs here
if not from_folder.exists() or not to_folder.exists():
    print("Source and/or Destination folder not correct. Program aborted.")
    sys.exit()

#  PREPARE sqlite3 for use in memory
def sqlite3_setup():
    """ create a database connection to an SQLite database """
    global con
    global CurObj
    try:
        con = sq.connect(":memory:", detect_types=sq.PARSE_DECLTYPES|sq.PARSE_COLNAMES)
        print("connected to sqlite3 version: ", sq.version, "\n")
        # if we don't create using memory we have to create a file on disk elsewhere
        #  and that would unnessarily complicate the demo
        CurObj = con.cursor()  # with connection in place (this example in memory) we need a cursor object for access
        # and with the cursor object in place we can create a table for our data
        CurObj.execute('''CREATE TABLE music(id integer PRIMARY KEY, SourcePath text ,title text, artist text)''')
    except Error as e:
        print(e)
        sys.exit()  # stop, cease and desist - somethin done busted

#  GET header data, primary key and path and store in Sqlite3 db
def grab_data():
    count = 0
    for root, dirs, files in os.walk(from_folder):
        for name in files:
            if (name.lower()).endswith(".mp3"):             
                src = p(root, name)
                #  src will now hold the full path of the (song) source file
                #  as a Pathlib object properly constructed for your os
                try:              
                    tag=(TinyTag.get(src))  # gets the song header in a dictionary-like
                    #  TinyTag object called tag, but header string it is not useable as is
                    #  for several reasons (1) null - no quotes, no cap (2) titles
                    #  starting with a number and (3) titles with multiple single quotes
                    #  (4) ' and - sometimes converted to \
                    count+=1
                    full_path = src
                    #  for each possilbe "key" in tag a method is provided by TinyTag 
                    #  to bring the data into a string with most issues resolved - 
                    #  possible problems/bugs with the "comment" key
                    title = tag.title  # TinyTag "keys"
                    if title == "" or title == None:
                        title = name
                    artist = tag.artist
                    if artist == "" or artist == None:
                        artist = "unavailable"
                    CurObj.execute('''insert into music VALUES(?,?,?,?)''',(count, str(src), title, artist))
                    #  sqlite does not support saving a Pathlib object so we convert it to a string
                    con.commit()
                except TinyTagException:
                    print("file error")  #probably a bad file
                    print(src)
                    sys.exit()

def cleantitle(title):
    title = title.replace("?","")
    title = title.replace("-","~")
    title = title.replace("/"," ")
    title = title.replace('"',' ')
    title = title.replace(":","~")
    return title

def nonum_starts(trackname):
    it1 = iter(trackname)
    for char in range(0, len(trackname)):
        itchar = next(it1)
        if not itchar.isalpha():
            continue
        else:
            trackname = trackname[char:]
            return(trackname)
            break
    

# MAIN PROGRAM PART 1 - initialize db, get data into db
sqlite3_setup()
grab_data() #get everything we can know from all song headers and put it in our db in memory

# ______________________________________

#  MAIN PROGRAM PART 2 - Use sqlite3 info which is still in memory and active
#  to move songs with artist attached to name into a single folder
print("Recalling from sqlite db:")
count = 0
# two lines that follow TEST either * for all, or column data by name
# CurObj.execute("select artist, title from music")
CurObj.execute("select * from music")  # get everything

# this section gets all records
records = (CurObj.fetchall())
rownum= len(records)
tracks = len(records)
print("total rows: ", rownum, "\n")

# Copying Tracks in folders/sub-folders
trkid, src_path, title, artist = 0, 1, 2, 3
count = 0
badcount = 0
for tracks in range(0,rownum):
    # if a title or artist info is null then header title blows up
    '''
    if records[tracks][title] == None or records[tracks][artist] == None:
        print("Skipping: " + str(records[tracks][src_path]))
        continue
    '''
    if records[tracks][title] == None:
        print("Skipping: " + str(records[tracks][src_path]) + " no Title")
        continue
    if records[tracks][artist] == None:
        print("Skipping: " + str(records[tracks][src_path]) + " no Artist")
        continue    
    newname = (records[tracks][title] + " ~ " + records[tracks][artist] + ".mp3")
    newname = cleantitle(newname)
    if newname[0].isdigit():
        newname = nonum_starts(newname)
    #dest = p(to_folder + "\\" + newname)
    dest = os.path.join(to_folder,newname)
    srcpath = p(records[tracks][src_path])


    try:
        srcpath.exists()
        count +=1
    except:
        print("This path does not exist: \n" + str(srcpath) +"\n" + "Skiping this file.")
        badcount +=1
        continue
    try:
        shutil.copyfile((srcpath), (dest))
    except FileNotFoundError:
        print("Not found - srcpath string: " + str(srcpath))
        print("TinyTag title in db: " + records[tracks][title])
        print("Using new name: " + str(dest) + "\n")
        badcount +=1
    except OSError:
        print("OSError - Could not be copied - srcpath: " + str(srcpath))
        print("TinyTag title in db: " + records[tracks][title])
        print("Using dest path with new name: " + str(dest) + "\n")
        badcount +=1
        
print("Copy Process Completed") 
print("Processed: " + str(count) + " successfully!")
print("Failed to process: " + str(badcount))

con.close()   # close the database connection before the program ends