asteroid/scripts/scan.py

223 lines
7.7 KiB
Python

#!/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('<I', f.read(4))[0]
if chunk_id == b'fmt ':
fmt_data = f.read(chunk_size)
sample_rate = struct.unpack('<I', fmt_data[4:8])[0]
byte_rate = struct.unpack('<I', fmt_data[8:12])[0]
break
else:
f.seek(chunk_size, 1)
# Find data chunk
while True:
chunk_id = f.read(4)
if len(chunk_id) < 4:
return None
chunk_size = struct.unpack('<I', f.read(4))[0]
if chunk_id == b'data':
if byte_rate > 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()