Written by Gemini. ```p Could you express that as CSV with pipe | as the separator, the last columns should be track duration and start time. If I have individual tracks ripped with itunes, could you write a python script (using ffprobe) to compile such a table from a list of filenames. Could you use a main(), add argparse. An option, -e or --end, will add an END entry at the end with just track title END, no track number, and the CD duration as the cumulative time. A -m option or --ptmd for long, will prepend the Track|... line with "csvhead:", and wrap the result with "```csv sep=|" before, and "```" afterwards to use in a modified markdown. ``` ```py #!/usr/bin/env python3 import argparse import json import os import subprocess import sys # Supported audio extensions (iTunes commonly uses m4a, mp3, wav, flac, etc.) AUDIO_EXTENSIONS = ('.m4a', '.mp3', '.flac', '.wav', '.aac', '.alac') def get_track_metadata(filepath): """Uses ffprobe to extract track title, artist, track number, and duration.""" cmd = [ 'ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filepath, ] try: result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) data = json.loads(result.stdout) except Exception as e: print(f'Error reading {filepath}: {e}', file=sys.stderr) return None format_data = data.get('format', {}) tags = format_data.get('tags', {}) # Normalize tag keys to lowercase for flexible lookup across ID3/iTunes tags tags_lower = {k.lower(): v for k, v in tags.items()} artist = ( tags_lower.get('artist') or tags_lower.get('album_artist') or 'Unknown Artist' ) title = ( tags_lower.get('title') or os.path.splitext(os.path.basename(filepath))[0] ) track_num = tags_lower.get('track', '') # Clean up track numbers like "1/16" -> "01" if '/' in track_num: track_num = track_num.split('/')[0] try: track_num = f'{int(track_num):02d}' except ValueError: track_num = None duration = float(format_data.get('duration', 0.0)) return { 'filepath': filepath, 'track': track_num, 'artist': artist, 'title': title, 'duration_sec': duration, } def format_seconds(seconds): """Converts seconds into MM:SS format.""" m, s = divmod(int(round(seconds)), 60) return f'{m:02d}:{s:02d}' def process_audio_files(target_paths, include_end=False, ptmd=False): """Reads audio tracks, calculates timings, and prints formatted CSV output.""" # Expand directory inputs to file lists files = [] for path in target_paths: if os.path.isdir(path): dir_files = [ os.path.join(path, f) for f in sorted(os.listdir(path)) if f.lower().endswith(AUDIO_EXTENSIONS) ] files.extend(dir_files) elif os.path.exists(path): files.append(path) if not files: print('No valid audio files found.', file=sys.stderr) return tracks = [] for f in files: meta = get_track_metadata(f) if meta: tracks.append(meta) # Header prefix logic header_prefix = 'csvhead:' if ptmd else '' header = f'{header_prefix}Track|Artist|Title|Duration|Start Time' output_lines = [] if ptmd: output_lines.append('```csv sep=|') output_lines.append(header) cum_seconds = 0.0 for idx, track in enumerate(tracks, 1): # Fallback to sequential index if metadata track number is missing track_num = track['track'] if track['track'] else f'{idx:02d}' start_time_str = format_seconds(cum_seconds) duration_str = format_seconds(track['duration_sec']) output_lines.append( f"{track_num}|{track['artist']}|{track['title']}|{duration_str}|{start_time_str}" ) cum_seconds += track['duration_sec'] # Optional -e / --end marker line if include_end: end_time_str = format_seconds(cum_seconds) output_lines.append(f'||END||{end_time_str}') if ptmd: output_lines.append('```') # Output to stdout print('\n'.join(output_lines)) def main(): parser = argparse.ArgumentParser( description='Generate a pipe-separated CSV tracklist with start timestamps using ffprobe.' ) parser.add_argument( 'paths', metavar='PATH', nargs='+', help='Audio file paths or folder directory containing ripped tracks.', ) parser.add_argument( '-e', '--end', action='store_true', help='Append an END entry row with the total cumulative CD duration.', ) parser.add_argument( '-m', '--ptmd', action='store_true', help='Prepend "csvhead:" to the header line and wrap output in a ```csv sep=| codeblock.', ) args = parser.parse_args() process_audio_files(args.paths, include_end=args.end, ptmd=args.ptmd) if __name__ == '__main__': main() ``` ### Example Output For Jarre's Chronologie: With no arguments we get ``` Track|Artist|Title|Duration|Start Time 01|Jean Michel Jarre|Chronologie Part 1|10:53|00:00 02|Jean Michel Jarre|Chronologie Part 2|06:05|10:53 03|Jean Michel Jarre|Chronologie Part 3|04:00|16:58 04|Jean Michel Jarre|Chronologie Part 4|03:59|20:58 05|Jean Michel Jarre|Chronologie Part 5|05:35|24:57 06|Jean Michel Jarre|Chronologie Part 6|03:46|30:32 07|Jean Michel Jarre|Chronologie Part 7|02:17|34:18 08|Jean Michel Jarre|Chronologie Part 8|05:33|36:35 ``` Then `-e` adds: ``` ||END||42:08 ``` With `-m -e` this results in markdown for this site: ````plaintext ```csv sep=| csvhead:Track|Artist|Title|Duration|Start Time 01|Jean Michel Jarre|Chronologie Part 1|10:53|00:00 02|Jean Michel Jarre|Chronologie Part 2|06:05|10:53 03|Jean Michel Jarre|Chronologie Part 3|04:00|16:58 04|Jean Michel Jarre|Chronologie Part 4|03:59|20:58 05|Jean Michel Jarre|Chronologie Part 5|05:35|24:57 06|Jean Michel Jarre|Chronologie Part 6|03:46|30:32 07|Jean Michel Jarre|Chronologie Part 7|02:17|34:18 08|Jean Michel Jarre|Chronologie Part 8|05:33|36:35 ||END||42:08 ``` ```` which renders as ```csv sep=| csvhead:Track|Artist|Title|Duration|Start Time 01|Jean Michel Jarre|Chronologie Part 1|10:53|00:00 02|Jean Michel Jarre|Chronologie Part 2|06:05|10:53 03|Jean Michel Jarre|Chronologie Part 3|04:00|16:58 04|Jean Michel Jarre|Chronologie Part 4|03:59|20:58 05|Jean Michel Jarre|Chronologie Part 5|05:35|24:57 06|Jean Michel Jarre|Chronologie Part 6|03:46|30:32 07|Jean Michel Jarre|Chronologie Part 7|02:17|34:18 08|Jean Michel Jarre|Chronologie Part 8|05:33|36:35 ||END||42:08 ```