Split mp3 of album into individual tracks Recently, I've been transferring some of my old vinyl records to MP3 files.  One of the things I did to make some progress was to automate things, so I initially set things up so that it would record each album to a single MP3 file.  Now I've gotten to the step where I want to separate those out into individual tracks, so I wrote this program to do that automatically.  It takes an input file in this format:

infile=~/vinyl/UK.mp3
artist=UK
genre=Rock
album=UK

1 - 00:00 - In the Dead Of Night     
2 - 05:38 - By the Light Of Day     
3 - 10:10 - Presto Vivace and Reprise     
4 - 13:18 - Thirty Years     
5 - 21:30 - Alaska     
6 - 26:04 - Time to Kill     
7 - 31:02 - Nevermore     
8 - 39:20 - Mental Medication


It then takes the input file in this metadata file and splits it into named tracks adding the album, artist and genre into each MP3 file using ffmpeg.  Here's the script:

songsplit.py

#!/usr/bin/python3
import subprocess
import sys


class Track(object):
    def __init__(self, tracknum, starttime, name):
        self.tracknum = int(tracknum)
        self.starttime = starttime
        self.endtime = starttime
        self.name = name

    def setEnd(self, endtime):
        self.endtime = endtime

    def setargs(self, infile, metadata):
        args = ['ffmpeg', '-i', infile, '-ss', self.starttime]
        if self.endtime != self.starttime:
            args.extend(['-to', self.endtime])
        for key in metadata.keys():
            args.extend(['-metadata', "{}={}".format(key, metadata[key])])
        args.extend(['-metadata', "{}={}".format("track", self.tracknum)])
        args.extend(['-metadata', "{}={}".format("title", self.name)])
        args.append("{:02d} {}.mp3".format(self.tracknum, self.name))
        return args

    def __repr__(self):
        return "{}, {}, {}, {}".format(self.tracknum, self.starttime,
                                       self.endtime, self.name)


def extract(file):
    metadata = {}
    tracks = []
    lines = [line.strip("\n") for line in open(file)]
    for line in lines:
        if line.find('=') &gt; 0:
            m = line.split(sep='=', maxsplit=1)
            if m[0] == 'infile':
                infile = m[1]
            else:
                metadata[m[0]] = m[1]
        else:
            m = line.split(sep=" - ", maxsplit=2)
            if len(m) == 3:
                m = [x.strip() for x in m]
                tk = Track(m[0], m[1], m[2])
                tracknum = int(tk.tracknum)
                if tracknum &gt; 1:
                    tracks[tracknum-2].setEnd(tk.starttime)
                tracks.append(tk)
    for track in tracks:
        subprocess.run(track.setargs(infile, metadata))


if __name__ == "__main__":
    if len(sys.argv) &lt; 2:
        print("Usage: {} metafile".format(sys.argv[0]))
    else:
        extract(sys.argv[1])


If the metadata file shown above is named UK.meta, then this would be invoked as songsplit.py UK.meta and the result would be a directory of MP3 files like this:

01 In the Dead Of Night.mp3
02 By the Light Of Day.mp3
03 Presto Vivace and Reprise.mp3
04 Thirty Years.mp3
05 Alaska.mp3
06 Time to Kill.mp3
07 Nevermore.mp3
08 Mental Medication.mp3


Some particular questions I have are about the use of subprocess.run() and what error handling I should add.  So far, I haven't encountered any problems.
I'll consider answering your last question (at least at the moment) about subprocess.run(). 

There's a similar method you can use for this kind of situations named subprocess.check_call(). The difference between the two is that call() already gives you the return code —
 not as an attribute, just as the return value of the function. The reason you might want to use check_call() is that it takes care of checking that return code for you, raising a CalledProcessError. 

Anyway, either one will raise an OSError for the program not being found.

So, a first solution to your problem might look like this:

try:
    subprocess.check_call(track.setargs(infile, metadata))
except subprocess.CalledProcessError:
    pass  # handle errors in the called executable
except OSError:
    pass  # executable not found


Now, I don't necessary like this solution because sincerely I don't know about those specific exception cases, or check_call() / run() (which apparently they both block). What I learned from my previous projects is that .run() basically merges all the old functions — call(), check_call(), check_output() — into one friendlier function.



You should know that subprocess will raise an exception, OSError, when a command is not found. So, if you catch OSError you can handle the non-existent command, and if you check the result code you can find out whether the command succeeded or not.

As a personal advice, If you have no good way to handle it, let it raise. If you want to continue the execution even if this fails, log it somewhere:

try:
    subprocess.run(track.setargs(infile, metadata))
except OSError as e:
    logger.WARNING("Error: {}".format(e))


Depending on what track.setargs() raises, you can change the Error type. 

It's worth mentioning that you might want to use except Exception which will cover any runtime exceptions. With except Exception you can remove the OSError except since Exception will cover those as well. This way of exception handling is kind of hand wavey (and it's best avoided) but will keep your program running.



Other aspects:


you're reading the entire file into memory before processing it.  If the file is very large, that will be slow; and if it's very large, it'll fail. Consider reading the file line by line.
I wouldn't bother writing a setEnd method. Calling it is no simpler than just saying tk.endtime = 3.
you can test if a string has '=' with:   if '=' in line:


PEP8


methods are usually snake_cased so if you'll keep your setEnd(self, endtime) it should be renamed to set_end(self, endtime)


Overall, nice job! You did pretty well! Keep going the good work.