Dup Goto 📝

CdTracklistGenerator_001

PT2/lang/python/media 07-23 05:49:10
To Pop
225 lines, 704 words, 6870 chars Thursday 2026-07-23 05:49:10

Written by Gemini.

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.
#!/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:

```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

TrackArtistTitleDurationStart Time
01Jean Michel JarreChronologie Part 110:5300:00
02Jean Michel JarreChronologie Part 206:0510:53
03Jean Michel JarreChronologie Part 304:0016:58
04Jean Michel JarreChronologie Part 403:5920:58
05Jean Michel JarreChronologie Part 505:3524:57
06Jean Michel JarreChronologie Part 603:4630:32
07Jean Michel JarreChronologie Part 702:1734:18
08Jean Michel JarreChronologie Part 805:3336:35
END42:08