#!/usr/bin/env python3 """ Author: Michal Szymanski v1.0 lastfm_wrapped.py - Generate Spotify Wrapped-style statistics from lastfm_monitor CSV data This tool analyzes CSV files generated by lastfm_monitor.py and provides statistics similar to Spotify Wrapped, including top artists, tracks and albums. """ import csv import argparse import sys from datetime import datetime, date from collections import Counter from pathlib import Path def parse_date(date_str): date_formats = [ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d", "%d %b %Y, %H:%M:%S", ] for fmt in date_formats: try: return datetime.strptime(str(date_str).strip(), fmt) except (ValueError, AttributeError): continue try: return datetime.fromisoformat(str(date_str).strip().replace('Z', '+00:00')) except (ValueError, AttributeError): pass raise ValueError(f"Unable to parse date: {date_str}") def read_csv_data(csv_file): data = [] try: with open(csv_file, 'r', encoding='utf-8') as f: sample = f.read(1024) f.seek(0) reader = csv.DictReader(f) required_columns = ['Date', 'Artist', 'Track', 'Album'] fieldnames = reader.fieldnames or [] if not all(col in fieldnames for col in required_columns): raise ValueError( f"CSV file must contain columns: {', '.join(required_columns)}. " f"Found: {', '.join(fieldnames)}" ) for row in reader: try: date_obj = parse_date(row['Date']) data.append({ 'date': date_obj, 'artist': row['Artist'].strip() if row['Artist'] else '', 'track': row['Track'].strip() if row['Track'] else '', 'album': row['Album'].strip() if row['Album'] else '', }) except ValueError as e: print(f"Warning: Skipping row with invalid date: {e}", file=sys.stderr) continue except FileNotFoundError: raise FileNotFoundError(f"CSV file not found: {csv_file}") except Exception as e: raise RuntimeError(f"Error reading CSV file: {e}") return data def filter_by_date_range(data, start_date, end_date, end_inclusive=False): filtered = [] for entry in data: entry_date = entry['date'] if end_inclusive: entry_date_only = entry_date.date() start_date_only = start_date.date() end_date_only = end_date.date() if start_date_only <= entry_date_only <= end_date_only: filtered.append(entry) else: if start_date <= entry_date < end_date: filtered.append(entry) return filtered def get_default_date_range(): now = datetime.now() current_year = now.year start_date = datetime(current_year, 1, 1) end_date = datetime(current_year, 11, 15) return start_date, end_date def calculate_top_items(data, top_n=5): artists = Counter() tracks = Counter() albums = Counter() for entry in data: if entry['artist']: artists[entry['artist']] += 1 if entry['track']: if entry['artist']: track_key = f"{entry['artist']} - {entry['track']}" else: track_key = entry['track'] tracks[track_key] += 1 if entry['album']: albums[entry['album']] += 1 top_artists = artists.most_common(top_n) top_tracks = tracks.most_common(top_n) top_albums = albums.most_common(top_n) return top_artists, top_tracks, top_albums def format_date_range(start_date, end_date): start_str = start_date.strftime("%B %d, %Y") end_str = end_date.strftime("%B %d, %Y") return f"{start_str} to {end_str}" def print_statistics(csv_file, top_artists, top_tracks, top_albums, start_date, end_date, total_scrobbles): print("=" * 70) print("šŸŽµ Last.fm Wrapped šŸŽµ") print("=" * 70) print(f"\nšŸ“ Source: {csv_file}") print(f"šŸ“… Period: {format_date_range(start_date, end_date)}") print(f"šŸŽ§ Total Scrobbles: {total_scrobbles:,}") print() if top_artists: print("šŸ† Top Artists:") print("-" * 70) for i, (artist, count) in enumerate(top_artists, 1): print(f" {i}. {artist:<50} {count:>6} plays") print() if top_tracks: print("šŸŽµ Top Tracks:") print("-" * 70) for i, (track, count) in enumerate(top_tracks, 1): display_track = track[:55] + "..." if len(track) > 58 else track print(f" {i}. {display_track:<55} {count:>6} plays") print() if top_albums: print("šŸ’æ Top Albums:") print("-" * 70) for i, (album, count) in enumerate(top_albums, 1): display_album = album[:55] + "..." if len(album) > 58 else album print(f" {i}. {display_album:<55} {count:>6} plays") print() print("=" * 70) def parse_date_arg(date_str): date_formats = [ "%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y", "%d/%m/%Y", "%Y-%m-%d %H:%M:%S", ] for fmt in date_formats: try: return datetime.strptime(date_str, fmt) except ValueError: continue raise ValueError(f"Unable to parse date: {date_str}. Use format YYYY-MM-DD") def main(): parser = argparse.ArgumentParser( description="Generate Spotify Wrapped-style statistics from Last.fm CSV data", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Default: Current year's wrapped (Jan 1 to Nov 15 of current year, like Spotify) python lastfm_wrapped.py data.csv # Custom date range python lastfm_wrapped.py data.csv --from 2024-01-01 --to 2024-12-31 # Top 10 instead of top 5 python lastfm_wrapped.py data.csv --top-n 10 # Full year 2024 python lastfm_wrapped.py data.csv --from 2024-01-01 --to 2025-01-01 --top-n 10 """ ) parser.add_argument( 'csv_file', type=str, help='Path to CSV file generated by lastfm_monitor.py' ) parser.add_argument( '--top-n', type=int, default=5, metavar='N', help='Number of top items to display for each category (default: 5)' ) parser.add_argument( '--from', dest='from_date', type=str, metavar='DATE', help='Start date (format: YYYY-MM-DD). Default: Jan 1 of current year' ) parser.add_argument( '--to', dest='to_date', type=str, metavar='DATE', help='End date (format: YYYY-MM-DD). Default: Nov 15 of current year (mid-November, like Spotify)' ) args = parser.parse_args() if args.top_n < 1: print("Error: --top-n must be at least 1", file=sys.stderr) sys.exit(1) user_provided_dates = False if args.from_date and args.to_date: try: start_date = parse_date_arg(args.from_date) end_date = parse_date_arg(args.to_date) end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999) user_provided_dates = True except ValueError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if start_date >= end_date: print("Error: --from date must be before --to date", file=sys.stderr) sys.exit(1) elif args.from_date or args.to_date: print("Error: Both --from and --to must be specified together, or neither", file=sys.stderr) sys.exit(1) else: start_date, end_date = get_default_date_range() try: data = read_csv_data(args.csv_file) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if not data: print("Error: No data found in CSV file", file=sys.stderr) sys.exit(1) filtered_data = filter_by_date_range(data, start_date, end_date, end_inclusive=user_provided_dates) if not filtered_data: print(f"Warning: No data found in the specified date range ({format_date_range(start_date, end_date)})", file=sys.stderr) print(f"Total records in CSV: {len(data)}", file=sys.stderr) print(f"Date range in CSV: {data[0]['date'].strftime('%Y-%m-%d')} to {data[-1]['date'].strftime('%Y-%m-%d')}", file=sys.stderr) sys.exit(1) top_artists, top_tracks, top_albums = calculate_top_items(filtered_data, args.top_n) print_statistics( args.csv_file, top_artists, top_tracks, top_albums, start_date, end_date, len(filtered_data) ) if __name__ == '__main__': main()