import os import shutil import exiftool import csv import subprocess def getFiles(): # Get list of files in STREAM dir filesArray = [] for file in os.listdir("./STREAM/"): filesArray.append("./STREAM/" + file) print("found file " + file) return filesArray def getMetadata(filesArray): with exiftool.ExifToolHelper() as et: filteredArray = {} metadata = et.get_metadata(filesArray, "-u") for entry in metadata: fileName = entry.get("SourceFile") userBit = entry.get("H264:MDPM_0x0014") timecode = entry.get("H264:TimeCode") filteredArray[fileName] = [userBit, timecode] return filteredArray def orderUserBit(filteredArray): for key in filteredArray: cleanValue = [ord(c) for c in filteredArray[key][0]] filteredArray[key][0] = cleanValue return filteredArray def applyMetadataDict(fixedDict): for key in fixedDict: metaDict = {} userBitArray = fixedDict[key][0] metaDict["Act"] = userBitArray[3] if userBitArray else 1 metaDict["Scene"] = userBitArray[2] if userBitArray else 1 metaDict["Reel"] = userBitArray[1] if userBitArray else 1 metaDict["Shot"] = userBitArray[0] if userBitArray else 1 fixedDict[key][0] = metaDict return fixedDict def addTakeNumbers(clipDict): # 1. Convert dict to a list of items to allow sorting itemList = [] for path, data in clipDict.items(): itemList.append({"path": path, "metadata": data[0], "timecode": data[1]}) # 2. Sort by timecode string (HH:MM:SS:FF) to ensure chronological order itemList.sort(key=lambda x: x["timecode"]) # 3. Track occurrences of Reel and Shot combinations shotCounts = {} newDict = {} for item in itemList: meta = item["metadata"] # Create a unique key for the specific Reel and Shot shotKey = f"{meta['Reel']}_{meta['Shot']}" # 4. Increment the take number based on previous occurrences takeNumber = shotCounts.get(shotKey, 0) + 1 shotCounts[shotKey] = takeNumber # 5. Construct the updated metadata dictionary # This keeps 'Take' right after 'Shot' updatedMeta = { "Act": meta["Act"], "Scene": meta["Scene"], "Reel": meta["Reel"], "Shot": meta["Shot"], "Take": takeNumber, } # Map it back to the original file path newDict[item["path"]] = [updatedMeta, item["timecode"]] return newDict def addGoodKey(clipDict): # 1. Group clips by (Reel, Scene, Shot) to find the max take for each specific setup # Key: (Reel, Scene, Shot), Value: highestTakeFound maxTakeTracker = {} for path, data in clipDict.items(): meta = data[0] # Adding Shot to the grouping key is the fix here groupKey = (meta["Reel"], meta["Scene"], meta["Shot"]) currentTake = meta["Take"] if groupKey not in maxTakeTracker or currentTake > maxTakeTracker[groupKey]: maxTakeTracker[groupKey] = currentTake # 2. Apply the 'Good' key based on the tracker newDict = {} for path, data in clipDict.items(): meta = data[0] timecode = data[1] groupKey = (meta["Reel"], meta["Scene"], meta["Shot"]) # Mark True if it's the highest take for that specific Shot isGood = meta["Take"] == maxTakeTracker[groupKey] # Update the metadata dictionary updatedMeta = { "Act": meta["Act"], "Scene": meta["Scene"], "Reel": meta["Reel"], "Shot": meta["Shot"], "Take": meta["Take"], "Good": isGood, } newDict[path] = [updatedMeta, timecode] return newDict def makeFileNameDict(infoDict): fileNameDict = {} for key in infoDict: meta = infoDict[key][0] act = meta["Act"] scene = meta["Scene"] reel = meta["Reel"] shot = meta["Shot"] take = meta["Take"] good = meta["Good"] if good == True: isGood = "_GOOD" else: isGood = "" fileName = ( f"R{reel}S{shot}.{take}-ACT{act}_SCENE{scene}_SHOT{shot}_TAKE{take}{isGood}" ) fileNameDict[key] = [meta, fileName] print(f"Named file {key} {fileName}") return fileNameDict def setFilePath(fileNameDict): os.makedirs("./output", exist_ok=True) for file in fileNameDict: fileName = "./output/" + fileNameDict[file][1] + ".mov" fileNameDict[file][1] = fileName return fileNameDict def copyFiles(filePathDict): for file in filePathDict: command = [ "ffmpeg", "-hide_banner", "-v", "error", # Hide everything except errors "-stats", # Force FFmpeg to print the progress line "-i", file, "-c:v", "copy", "-c:a", "pcm_s16le", "-ar", "48000", "-y", filePathDict[file][1], ] try: print(f"\nTranscoding {file} to {filePathDict[file][1]}...") # REMOVED capture_output=True so it streams directly to your terminal subprocess.run(command, check=True) print(f"Finished: {filePathDict[file][1]}") except subprocess.CalledProcessError as e: print(f"FFmpeg failed for {file}") def generateResolveCsv(clipDict, outputCsvPath): """ Generates a CSV file for DaVinci Resolve metadata import. Matches metadata to the new .mov files based on the file name. """ # Column names that DaVinci Resolve recognizes natively # 'File Name' is our primary key for matching fieldnames = [ "File Name", "Reel Number", "Scene", "Shot", "Take", "Good Take", "Rating", "Keywords", ] try: with open(outputCsvPath, mode="w", newline="", encoding="utf-8") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for sourcePath, data in clipDict.items(): meta = data[0] # data[1] is our output path (e.g., './output/R1S1.1-ACT1...mov') outputPath = data[1] # We use the basename so Resolve can match it to clips in the Media Pool fileName = os.path.basename(outputPath) writer.writerow( { "File Name": fileName, "Reel Number": f"{meta['Reel']}", "Scene": str(meta["Scene"]), "Shot": str(meta["Shot"]), "Take": str(meta["Take"]), "Good Take": "1" if meta["Good"] else "0", "Rating": "5" if meta["Good"] else "0", "Keywords": f"Reel {meta['Reel']}", } ) print(f"Successfully generated metadata CSV: {outputCsvPath}") except Exception as e: print(f"Error generating CSV: {e}") files = getFiles() print() userBitDict = getMetadata(files) fixedUBDict = orderUserBit(userBitDict) metadataDict = applyMetadataDict(fixedUBDict) takesDict = addTakeNumbers(metadataDict) final = addGoodKey(takesDict) fileNames = makeFileNameDict(final) filePaths = setFilePath(fileNames) copyFiles(filePaths) generateResolveCsv(filePaths, "./out.csv")