#!/usr/bin/env python3 """ Generate a tree view of the music library with track durations No external dependencies required - reads file headers directly Usage: python3 music-library-tree-standalone.py [music-directory] [output-file] """ import os import sys import struct from pathlib import Path from datetime import datetime AUDIO_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.m4a', '.wav', '.aac', '.opus', '.wma'} def get_mp3_duration(file_path): """Get MP3 duration by reading frame headers""" try: with open(file_path, 'rb') as f: # Skip ID3v2 tag if present header = f.read(10) if header[:3] == b'ID3': size = struct.unpack('>I', b'\x00' + header[6:9])[0] f.seek(size + 10) else: f.seek(0) # Read first frame to get bitrate and sample rate frame_header = f.read(4) if len(frame_header) < 4: return None # Parse MP3 frame header if frame_header[0] != 0xFF or (frame_header[1] & 0xE0) != 0xE0: return None # Bitrate table (MPEG1 Layer III) bitrates = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0] bitrate_index = (frame_header[2] >> 4) & 0x0F bitrate = bitrates[bitrate_index] * 1000 if bitrate == 0: return None # Get file size f.seek(0, 2) file_size = f.tell() # Estimate duration duration = (file_size * 8) / bitrate return int(duration) except: return None def get_flac_duration(file_path): """Get FLAC duration by reading metadata block""" try: with open(file_path, 'rb') as f: # Check FLAC signature if f.read(4) != b'fLaC': return None # Read metadata blocks while True: block_header = f.read(4) if len(block_header) < 4: return None is_last = (block_header[0] & 0x80) != 0 block_type = block_header[0] & 0x7F block_size = struct.unpack('>I', b'\x00' + block_header[1:4])[0] if block_type == 0: # STREAMINFO streaminfo = f.read(block_size) # Sample rate is at bytes 10-13 (20 bits) sample_rate = (struct.unpack('>I', streaminfo[10:14])[0] >> 12) & 0xFFFFF # Total samples is at bytes 13-17 (36 bits) total_samples = struct.unpack('>Q', b'\x00\x00\x00' + streaminfo[13:18])[0] & 0xFFFFFFFFF if sample_rate > 0: duration = total_samples / sample_rate return int(duration) return None if is_last: break f.seek(block_size, 1) except: return None def get_wav_duration(file_path): """Get WAV duration by reading RIFF header""" try: with open(file_path, 'rb') as f: # Check RIFF header if f.read(4) != b'RIFF': return None f.read(4) # File size if f.read(4) != b'WAVE': return None # Find fmt chunk while True: chunk_id = f.read(4) if len(chunk_id) < 4: return None chunk_size = struct.unpack(' 0: duration = chunk_size / byte_rate return int(duration) return None else: f.seek(chunk_size, 1) except: return None def get_duration(file_path): """Get duration of audio file by reading file headers""" ext = file_path.suffix.lower() if ext == '.mp3': duration_sec = get_mp3_duration(file_path) elif ext == '.flac': duration_sec = get_flac_duration(file_path) elif ext == '.wav': duration_sec = get_wav_duration(file_path) else: # For other formats, we can't easily read without libraries return "--:--" if duration_sec is not None: minutes = duration_sec // 60 seconds = duration_sec % 60 return f"{minutes:02d}:{seconds:02d}" return "--:--" def format_size(size): """Format file size in human-readable format""" for unit in ['B', 'KB', 'MB', 'GB']: if size < 1024.0: return f"{size:.2f} {unit}" size /= 1024.0 return f"{size:.2f} TB" def build_tree(directory, output_file, prefix="", is_last=True): """Recursively build tree structure""" try: entries = sorted(Path(directory).iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())) except PermissionError: return for i, entry in enumerate(entries): is_last_entry = (i == len(entries) - 1) connector = "└── " if is_last_entry else "├── " if entry.is_dir(): output_file.write(f"{prefix}{connector}📁 {entry.name}/\n") extension = " " if is_last_entry else "│ " build_tree(entry, output_file, prefix + extension, is_last_entry) else: ext = entry.suffix.lower() if ext in AUDIO_EXTENSIONS: duration = get_duration(entry) size = entry.stat().st_size size_fmt = format_size(size) output_file.write(f"{prefix}{connector}🎵 {entry.name} [{duration}] ({size_fmt})\n") else: output_file.write(f"{prefix}{connector}📄 {entry.name}\n") def main(): music_dir = sys.argv[1] if len(sys.argv) > 1 else "/home/glenneth/Music" output_path = sys.argv[2] if len(sys.argv) > 2 else "music-library-tree.txt" music_path = Path(music_dir) if not music_path.exists(): print(f"Error: Music directory '{music_dir}' does not exist") sys.exit(1) print("Generating music library tree...") print("Reading durations from file headers (MP3, FLAC, WAV supported)") # Count audio files audio_files = [] for ext in AUDIO_EXTENSIONS: audio_files.extend(music_path.rglob(f"*{ext}")) total_audio = len(audio_files) # Generate tree with open(output_path, 'w', encoding='utf-8') as f: f.write("Music Library Tree\n") f.write("==================\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Directory: {music_dir}\n") f.write(f"Duration support: MP3, FLAC, WAV (no external libraries needed)\n") f.write(f"\nTotal audio files: {total_audio}\n\n") f.write(f"📁 {music_path.name}/\n") build_tree(music_path, f, "", True) print(f"\nTree generated successfully!") print(f"Output saved to: {output_path}") print(f"Total audio files: {total_audio}") if __name__ == "__main__": main()