|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +import subprocess, argparse, os, os.path, random, string, tempfile, csv, re, shutil |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +FFMPEG = "/usr/local/bin/ffmpeg" |
| 7 | +MEZZANINE = "-acodec libfdk_aac -vbr 5 -ac 2 -map 0:a" |
| 8 | +MD5HashRE = re.compile(r'(?i)(?<![a-z0-9])[a-f0-9]{32}(?![a-z0-9])') |
| 9 | + |
| 10 | +def analyse(filename, mezzanine=None, forceEncode=False): |
| 11 | + # Encode and store a mezzanine file, if a mezzanine directory name is given |
| 12 | + |
| 13 | + print("Processing filename: %s" % filename) |
| 14 | + # If we're being asked to create a mezzanine file, we need to make a unique suffix for this file |
| 15 | + # but the suffix MUST be related to the file's contents, to be able to identify the file |
| 16 | + # so that we do not waste time encoding it twice. FFmpeg provides a hash function for this. |
| 17 | + # Incidentally, this might be a way of detecting duplicate tracks, too. |
| 18 | + if mezzanine: |
| 19 | + hashout = str(subprocess.check_output([FFMPEG, "-v", "quiet", "-hide_banner", "-i", filename, "-vn", \ |
| 20 | + "-map", "0:a", "-f", "hash", "-hash", "MD5", "-"], stderr=subprocess.STDOUT))[6:-3] |
| 21 | + |
| 22 | + print("MD5 hash is: %s" % hashout) |
| 23 | + #randomString = ''.join(random.choice(string.ascii_letters) for i in range(6)) |
| 24 | + |
| 25 | + # But! If this filename already contains a hash, we need to remove it before adding this one. |
| 26 | + # A hash exists in the between the second-to-last . and the last . |
| 27 | + # and has 32 characters from 0-9,a-f |
| 28 | + |
| 29 | + origBaseName = os.path.basename(filename) |
| 30 | + searchCheck = re.search(MD5HashRE, origBaseName) |
| 31 | + print("Debug: searchcheck = %s" % searchCheck) |
| 32 | + |
| 33 | + if searchCheck: |
| 34 | + # Remove the existing hash |
| 35 | + # No action required if there isn't any hash in the first place |
| 36 | + baseNameNoHash = origBaseName.replace('.' + searchCheck.group(0), '') |
| 37 | + print("Debug: name without hash: %s" % baseNameNoHash) |
| 38 | + origBaseName = baseNameNoHash |
| 39 | + |
| 40 | + |
| 41 | + baseName = os.path.splitext(origBaseName)[0] + "." + hashout + ".mka" |
| 42 | + print("Debug: name with replaced hash: %s" % baseName) |
| 43 | + |
| 44 | + mezzanineName = os.path.join(mezzanine, baseName) |
| 45 | + print("Mezzanine name is: %s" % mezzanineName) |
| 46 | + |
| 47 | + # Now we must detect if this file has already been converted |
| 48 | + # What we're interested in comparing is the string between the penultimate '.' |
| 49 | + # and the final '.' |
| 50 | + # Create a list with any filenames matching the hash. |
| 51 | + # If it contains an entry, the track has already been converted, and we |
| 52 | + # need to abandon the process. |
| 53 | + print("Looking for file containing hash.") |
| 54 | + p = Path(mezzanine+'/') |
| 55 | + findMe = mezzanine + '/' + '[FILENAME]' + hashout + '*.mka' |
| 56 | + print("Path object is:", p) |
| 57 | + print("Glob search is:", findMe) |
| 58 | + pl = list(p.glob('*'+hashout+'*.mka')) |
| 59 | + if len(pl) != 0: # There is already a file with this hash |
| 60 | + print("This hash already exists! Not encoding.") |
| 61 | + return(None) |
| 62 | + print("No file found with that hash. Encoding.") |
| 63 | + |
| 64 | + else: |
| 65 | + mezzanineName = None |
| 66 | + |
| 67 | + # At this point, the file of interest is EITHER the original file, OR a mezzanine name. |
| 68 | + if mezzanine: |
| 69 | + print("Creating mezzanine file.") |
| 70 | + |
| 71 | + # We need a temporary filename for FFmpeg to write to. We can't write metadata in place, because |
| 72 | + # the position of other elements in the file would change. |
| 73 | + temporaryFile = tempfile.NamedTemporaryFile(delete=False).name + ".mka" |
| 74 | + test = str(subprocess.check_output([FFMPEG, "-hide_banner", "-i", filename, \ |
| 75 | + "-vn", "-acodec", "copy", temporaryFile], stderr=subprocess.STDOUT)).split('\\n') |
| 76 | + shutil.move(temporaryFile, mezzanineName) |
| 77 | + else: |
| 78 | + print("We are NOT creating a new file.") |
| 79 | + |
| 80 | + return({"mezzanine_name": mezzanineName}) |
| 81 | + |
| 82 | + |
| 83 | +# What's the command? |
| 84 | +parser = argparse.ArgumentParser(description="Create start and end-of-track annotations for playlist.", |
| 85 | + epilog="For support, contact john@johnwarburton.net") |
| 86 | +parser.add_argument("playlist", help="Playlist file to be processed") |
| 87 | +parser.add_argument("-o", "--output", help="Output filename (default: '-processed' suffix)", type=str) |
| 88 | +parser.add_argument("-m", "--mezzanine", help="Directory for mezzanine-format files", type=str) |
| 89 | +args = parser.parse_args() |
| 90 | + |
| 91 | +playlist = args.playlist |
| 92 | + |
| 93 | +# Construct default output filename if needed |
| 94 | +if args.output: |
| 95 | + outfile = args.output |
| 96 | +else: |
| 97 | + outfile = os.path.splitext(playlist)[0] + "-processed.m3u8" |
| 98 | + |
| 99 | +# Check mezzanine directory name and create if needed |
| 100 | +if args.mezzanine: |
| 101 | + # Convert given path to an absolute path |
| 102 | + mezzanine = os.path.abspath(args.mezzanine) |
| 103 | + try: |
| 104 | + os.makedirs(mezzanine, exist_ok=True) |
| 105 | + print("Created directory %s for output audio files." % mezzanine) |
| 106 | + except OSError: |
| 107 | + print("Sorry, the directory %s is weird. Might be a file?" % mezzanine) |
| 108 | + exit(1) |
| 109 | +else: |
| 110 | + mezzanine = None |
| 111 | + |
| 112 | +print("Working on playlist: %s" % playlist) |
| 113 | +print("Writing to %s" % outfile) |
| 114 | + |
| 115 | +with open(playlist) as i: |
| 116 | + playlistLines = i.readlines() |
| 117 | + |
| 118 | +print("We have read %s items." % len(playlistLines)) |
| 119 | + |
| 120 | +with open(outfile, mode="w") as out: |
| 121 | + out.write("#EXTM3U\n") |
| 122 | + |
| 123 | + for item in playlistLines: |
| 124 | + # Skip the M3U indicator |
| 125 | + if item == "#EXTM3U\n": |
| 126 | + continue |
| 127 | + result = analyse(filename=item.strip(), mezzanine=mezzanine, forceEncode=False) |
| 128 | + # analyse() returns None if the audio has already been converted. |
| 129 | + # At this point, we can skip writing a new line to the playlist, because the file is already |
| 130 | + # extant, and must have been referenced already within the playlist we're creating. |
| 131 | + if result==None: |
| 132 | + continue |
| 133 | + if result["mezzanine_name"]: |
| 134 | + # Remember, a file read in lines has a newline on the end of every line |
| 135 | + item = result["mezzanine_name"] + '\n' |
| 136 | + |
| 137 | + assembly = item |
| 138 | + print("Writing line:") |
| 139 | + print(assembly) |
| 140 | + out.write(assembly) |
| 141 | + # Fingerprinting is now in a separate program |
| 142 | + #fing = fingerprint(item.strip()) |
| 143 | + #fd = open('database.csv', 'a') |
| 144 | + #csvWriter = csv.writer(fd) |
| 145 | + #csvWriter.writerow([item.strip(), fing]) |
| 146 | + #fd.close() |
| 147 | + #print("Fingerprint is:") |
| 148 | + #print(fing) |
| 149 | +print("Done.") |
| 150 | + |
| 151 | + |
| 152 | + |
0 commit comments