You are not logged in.

#1 2009-02-05 01:28:28

Stalafin
Member
From: Berlin, Germany
Registered: 2007-10-26
Posts: 617

CLI tool to rename music files based on id3 tags?

I have just found an awesome tool to change id3v2 tags in the shell - id3v2. smile

Now I need a tool to mass rename my files based on their id3 tags. A tool that simply renames all files in one folder based on the scheme i provide would be sufficient.


Is there something like that out there?

Offline

#2 2009-02-05 03:04:30

kludge
Member
Registered: 2008-08-03
Posts: 294

Re: CLI tool to rename music files based on id3 tags?

i don't know of any tools to do it for you, but why not write a bash script?  a for-each loop against each of your mp3s that uses id3v2 to get the tag values into variable names and then something like 'mv ${oldfilename} $artist/$album/$tracknum.$title.mp3'.

this would actually be a really good intro to bash project.  in fact... i just might do it for me own collection... hmmm.


[23:00:16]    dr_kludge | i want to invent an olfactory human-computer interface, integrate it into the web standards, then produce my own forked browser.
[23:00:32]    dr_kludge | can you guess what i'd call it?
[23:01:16]    dr_kludge | nosilla.
[23:01:32]    dr_kludge | i really should be going to bed.  i'm giggling madly about that.

Offline

#3 2009-02-05 15:22:45

pointone
Wiki Admin
From: Waterloo, ON
Registered: 2008-02-21
Posts: 379

Re: CLI tool to rename music files based on id3 tags?

I wrote a simple Python script to clean up my collection last year. Try it on for size!

It requires the mutagen package, and only works with MP3s and OGGs at the moment. I've wanted to keep developing it for a while now, but I only have MP3s and OGGs in my collection, so there hasn't been much motivation on my part to expand its functionality.

#! /usr/bin/env python
#
# Desmond Cox
# April 10, 2008

"""Project Music

Renames audio files based on metadata

Usage: projectmusic.py [options]

Options:
  -d ...,   --directory=...             Specify which directory to work in 
                                        (default is the current directory)
  -f ...,   --format=...                Specify the naming format
  -l,       --flatten                   Move all files into the same root
                                        directory
  -r,       --recursive                 Work recursively on the specified 
                                        directory
  -t,       --test                      Only display the new file names; nothing
                                        will be renamed
  -h,       --help                      Display this help
  
Formatting:
  The following information is available to be used in the file name:
  album    artist    title    track
  
  To specify a file name format, enter the desired format enclosed in quotation
  marks. The words album, artist, title, and track will be replaced by values
  retrieved from the audio file's metadata.
  
  For example, --format="artist - album [track] title" will rename music files
  with the name format:
  Sample Artist - Sample Album [1] Sample Title
  
  The following characters are of special importance to the operating system 
  and cannot be used in the file name:
  \    /    :    *    ?    "    <    >    |

  (=) is replaced by the directory path separator, so to move files into
  artist and album subdirectories, the following format can be used:
  "artist(=)album(=)track - title"
  
  If no format is provided, the default format is the same as used in the above
  example.

Examples:
  projectmusic.py                       Renames music files in the current 
                                        directory
  projectmusic.py -d /music/path/       Renames music files in /music/path/
  projectmusic.py -f "title -- artist"  Renames music files in the current
                                        directory with the name format:
                                        Sample Title -- Sample Artist.mp3"""

### Imports ###

import time
import re
import os
import sys
import getopt

import mutagen.easyid3
import mutagen.oggvorbis

### Exceptions ###
    
class FormatError(Exception):
    """
    Exception raised due to improper formatting
    """
    pass

class DirectoryError(Exception):
    """
    Exception raised due to a non-existent directory
    """
    pass

### Definitions ###

def scanDirectory(directory, fileExtList, recursive=False):
    """
    Generate a list of files with the specified extension(s) in the specified 
    directory (and its subdirectories, if the recursive option is enabled) 
    """
    fileList = []

    for dirPath, dirNames, fileNames in os.walk(directory):
        for name in fileNames:
            if os.path.splitext(name)[1].lower() in fileExtList:
                # lower() is necessary here; otherwise ".MP3" is not considered
                # a valid extension and files will be skipped. The extension's
                # case is preserved when renaming the file, however
                fileList.append(os.path.normcase(os.path.join(dirPath, name)))

        if not recursive:
            break # do not continue to the next "dirPath"

    return fileList

class AudioFile:
    """
    A generic audio file 
    """
    def __init__(self, fileName):
        self.fileName = fileName
        self.fileExt = os.path.splitext(fileName)[1].lower()
        self.filePath = os.path.split(fileName)[0] + os.path.sep

        self.data = getattr(self, "parse_%s" % self.fileExt[1:])()
        # call the appropriate method based on the file type

        self.generate()

    def generate(self):
        def lookup(key, default):
            return self.data[key][0] if ( self.data.has_key(key) and 
                                          self.data[key][0] ) else default

        self.artist = lookup("artist", "No Artist")
        self.album = lookup("album", "No Album")
        self.title = lookup("title", "No Title")
        self.track = lookup("tracknumber", "0")

        if self.track != "0":
            self.track = self.track.split("/")[0].lstrip("0") 

        # In regards to track numbers, self.data["tracknumber"] returns numbers 
        # in several different formats: 1, 1/10, 01, or 01/10. Wanting a 
        # consistent format, the returned string is split at the "/" and leading
        # zeros are stripped.

    def parse_mp3(self):
        return mutagen.easyid3.EasyID3(self.fileName)

    def parse_ogg(self):
        return mutagen.oggvorbis.Open(self.fileName)

    def rename(self, newFileName, flatten=False):
        def uniqueName(newFileName, count=0):
            """
            Returns a unique name if a file already exists with the supplied 
            name
            """
            c = "_(%s)" % str(count) if count else ""
            prefix = directory + os.path.sep if flatten else self.filePath
            testFileName = prefix + newFileName + c + self.fileExt
    
            if os.path.isfile(testFileName):
                count += 1
                return uniqueName(newFileName, count)
    
            else:
                return testFileName
        
        os.renames(self.fileName, uniqueName(newFileName))
    
        # Note: this function is quite simple at the moment; it does not support
        # multiple file extensions, such as "sample.txt.backup", which would 
        # only retain the ".backup" file extension.

    def cleanFileName(self, format):
        """
        Generate a clean file name based on metadata
        """
        rawFileName = format % {"artist": self.artist,
                                "album": self.album,
                                "title": self.title,
                                "track": self.track}

        rawFileName.encode("ascii", "replace")
        # encode is used to override the default encode error-handing mode;
        # which is to raise a UnicodeDecodeError
    
        cleanFileName = re.sub(restrictedCharPattern, "+", rawFileName)
        # remove restricted filename characters (\, /, :, *, ?, ", <, >, |) from
        # the supplied string

        return cleanFileName.replace("(=)", os.path.sep)

### Main ###

def main(argv):
    global directory
    directory = os.getcwd()
    format = "%(artist)s - %(album)s [%(track)s] %(title)s"
    flatten = False
    recursive = False
    test = False
 
    def verifyFormat(format):
        """
        Verify the supplied filename format
        """    
        if re.search(restrictedCharPattern, format):
            raise FormatError, "supplied format contains restricted characters"

        if not re.search(formatPattern, format):
            raise FormatError, "supplied format does not contain any metadata keys"
            # the supplied format must contain at least one of "artist", 
            # "album", "title", or "track", or all files will be named 
            # identically
        
        format = format.replace("artist", "%(artist)s")
        format = format.replace("album", "%(album)s")
        format = format.replace("title", "%(title)s")
        format = format.replace("track", "%(track)s")
        return format
        
    def verifyDirectory(directory):
        """
        Verify the supplied directory path
        """
        if os.path.isdir(directory):
            return os.path.abspath(directory)
        
        else:
            raise DirectoryError, "supplied directory cannot be found"    

    def usage():
        print __doc__

    try:
        opts, args = getopt.getopt(argv, "d:f:hlrt", ["directory=", 
                                                      "format=", 
                                                      "help", 
                                                      "flatten", 
                                                      "recursive", 
                                                      "test"])
    
    except getopt.error, error:
        usage()
        print "\n***Error: %s***" % error
        sys.exit(1)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            usage()
            sys.exit()
        
        elif opt in ("-f", "--format"):
            try:
                format = verifyFormat(arg)
            
            except FormatError, error:
                print "\n***Error: %s***" % error
                sys.exit(2)
        
        elif opt in ("-d", "--directory"):
            try:
                directory = verifyDirectory(arg)
            
            except DirectoryError, error:
                print "\n***Error: %s***" % error
                sys.exit(3)

        elif opt in ("-l", "--flatten"):
            flatten = True

        elif opt in ("-r", "--recursive"):
            recursive = True
                
        elif opt in ("-t", "--test"):
            test = True

    work(directory, format, flatten, recursive, test)

def safety(message):
    print "\n***Attention: %s***" % message
    safety = raw_input("Enter 'ok' to continue (any other response will abort): ")
    
    if safety.lower().strip() != "ok":
        print "\n***Attention: aborting***"
        sys.exit()

def work(directory, format, flatten, recursive, test):
    fileList = scanDirectory(directory, [".mp3", ".ogg"], recursive)

    try:
        if test:
            safety("testing mode; nothing will be renamed")
    
            print "\n***Attention: starting***"
    
            for f in fileList:              
                current = AudioFile(f)
                print current.cleanFileName(format)
                    
        else:
            count = 0
            total = len(fileList)
            safety("all audio files in %s will be renamed" % directory)

            print "\n***Attention: starting***"
            start = time.time()
                
            for f in fileList:
                count += 1
                current = AudioFile(f)
                current.rename(current.cleanFileName(format), flatten)
                message = "Renamed %d of %d" % (count, total)
                sys.stdout.write("\r" + message)

            print "\n%d files renamed in %f seconds" % (len(fileList), 
                                                        time.time() - start)
   
    except StandardError:
        print "\n***Error: %s***" % f 
        raise
        
if __name__ == "__main__":
    restrictedCharPattern = re.compile('[\\\\/:\*\?"<>\|]')
    formatPattern = re.compile('artist|album|title|track')

    main(sys.argv[1:])

M*cr*s*ft: Who needs quality when you have marketing?

Offline

#4 2009-02-05 15:48:52

koch
Member
From: Germany
Registered: 2008-01-26
Posts: 369

Re: CLI tool to rename music files based on id3 tags?

tried it with a few files, worked.
just copiing my music to another backup directory to test it there with all the files.


edit: hmmm, it stops if a title has no id3 tag. is there a way to skip this song?

Last edited by koch (2009-02-05 16:01:26)

Offline

#5 2009-02-05 19:57:11

pointone
Wiki Admin
From: Waterloo, ON
Registered: 2008-02-21
Posts: 379

Re: CLI tool to rename music files based on id3 tags?

Care to post the error message? If a file is missing a specifc tag, it should fill in with "No Title", "No Album", or "No Artist", respectively.


M*cr*s*ft: Who needs quality when you have marketing?

Offline

#6 2011-04-09 09:23:42

Reap3r
Member
Registered: 2011-04-09
Posts: 1

Re: CLI tool to rename music files based on id3 tags?

Awesome little script. Works well but yes... get the same error as koch -

here is the error:

-------------------------------------------------
***Error: /home/user/Documents/HDD Dump/Music/f44240416.mp3***
Traceback (most recent call last):
  File "mp3.py", line 316, in <module>
    main(sys.argv[1:])
  File "mp3.py", line 267, in main
    work(directory, format, flatten, recursive, test)
  File "mp3.py", line 300, in work
    current = AudioFile(f)
  File "mp3.py", line 111, in __init__
    self.data = getattr(self, "parse_%s" % self.fileExt[1:])()
  File "mp3.py", line 135, in parse_mp3
    return mutagen.easyid3.EasyID3(self.fileName)
  File "/usr/lib/pymodules/python2.6/mutagen/easyid3.py", line 167, in __init__
    self.load(filename)
  File "/usr/lib/pymodules/python2.6/mutagen/id3.py", line 113, in load
    self.__load_header()
  File "/usr/lib/pymodules/python2.6/mutagen/id3.py", line 211, in __load_header
    raise ID3NoHeaderError("'%s' doesn't start with an ID3 tag" % fn)
mutagen.id3.ID3NoHeaderError: '/home/user/Documents/HDD Dump/Music/f44240416.mp3' doesn't start with an ID3 tag
------------------------------------------

and sorry for resurrecting such an old post....

Offline

#7 2016-05-24 22:58:47

tomjleo
Member
Registered: 2016-05-24
Posts: 1

Re: CLI tool to rename music files based on id3 tags?

I had this very problem and put together the following script python script https://gitlab.com/tomleo/id3_folder_rename. Hope this script is helpful to others who might google upon this page smile

Offline

#8 2016-05-24 23:41:04

WorMzy
Administrator
From: Scotland
Registered: 2010-06-16
Posts: 12,399
Website

Re: CLI tool to rename music files based on id3 tags?

Thanks for sharing, hopefully OP isn't still looking for a solution seven years on, but someone may find your post helpful. Still, I'm going to take this opportunity to close this old thread.


Closing.


Sakura:-
Mobo: MSI MAG X570S TORPEDO MAX // Processor: AMD Ryzen 9 5950X @4.9GHz // GFX: AMD Radeon RX 5700 XT // RAM: 32GB (4x 8GB) Corsair DDR4 (@ 3000MHz) // Storage: 1x 3TB HDD, 6x 1TB SSD, 2x 120GB SSD, 1x 275GB M2 SSD

Making lemonade from lemons since 2015.

Online

Board footer

Powered by FluxBB