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