#!/usr/bin/perl -w # Copyright © 2007 Jamie Zawinski # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. # # Created: 20-Oct-2007. require 5; use diagnostics; use strict; use MP3::Tag; my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.9 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/; my $verbose = 1; my $debug_p = 0; my $normalize_p = 1; my $datadir1 = "$ENV{HOME}/Music/Mixtapes/128"; my $datadir2 = "$ENV{HOME}/Music/Mixtapes/full"; sub safe_system(@) { my (@cmd) = @_; print STDOUT "$progname: executing " . join(' ', @cmd) . "\n" if ($verbose > 3); system @cmd; my $exit_value = $? >> 8; my $signal_num = $? & 127; my $dumped_core = $? & 128; error ("$cmd[0]: core dumped!") if ($dumped_core); error ("$cmd[0]: signal $signal_num!") if ($signal_num); error ("$cmd[0]: exited with $exit_value!") if ($exit_value); } sub playlist_contents($) { my ($name) = @_; my @script = ('tell application "iTunes"', ' set TS to every file track of playlist "' . $name . '"', ' set output to ""', ' repeat with T in TS', ' set L to the location of T', ' set output to output & ' . ' (the artist of T) & "\t" & ' . ' (the album of T) & "\t" & ' . ' (the year of T) & "\t" & ' . ' (the name of T) & "\t" & ' . ' (the time of T) & "\t" & ' . ' (the POSIX path of L) & "\n"', ' end repeat', 'end tell'); my $cmd = "osascript -e '" . join ("' -e '", @script) . "'"; print STDERR "$progname: talking to iTunes...\n" if ($verbose); my $lines = `$cmd`; error ("playlist \"$name\" is empty") unless ($lines =~ m/^.{40}/); return split (/\n/, $lines); } sub mixtape_install($$) { my ($playlist_name, $twop) = @_; my $dir_name0 = $playlist_name; $dir_name0 =~ s/ .*$//s ; $dir_name0 =~ s/^mixtape\s*//si; $dir_name0 =~ s/\s+/_/gsi; $dir_name0 = lc($dir_name0); my $dir_name1 = "$datadir1/$dir_name0"; my $dir_name2 = "$datadir2/$dir_name0"; error ("$datadir1/ does not exist") unless (-d $datadir1 || !$twop); error ("$datadir2/ does not exist") unless (-d $datadir2); error ("$dir_name1/ already exists") if (-e $dir_name1 && !$twop); error ("$dir_name2/ already exists") if (-e $dir_name2 && !$debug_p); my @src_files = playlist_contents ($playlist_name); if (! -d $dir_name1 && !$twop) { mkdir $dir_name1 || error ("mkdir $dir_name1: $!") unless $debug_p; print STDERR "$progname: creating $dir_name1/...\n" if ($verbose); } if (! -d $dir_name2) { mkdir $dir_name2 || error ("mkdir $dir_name2: $!") unless $debug_p; print STDERR "$progname: creating $dir_name2/...\n" if ($verbose); } my @dest_files1 = (); my @dest_files2 = (); my $i = 1; my @artist; my @name; my @year; foreach (@src_files) { my ($artist, $album, $year, $name, $time, $from) = split (/\t/, $_); $artist[$i] = $artist; $name[$i] = $name; $year[$i] = $year; # mid-truncate artist and name to N*2 characters my $n = 20; my ($artist2, $name2) = ($artist, $name); $artist2 =~ s/^(.{$n}).{4}.*(.{$n})$/$1...$2/s; $name2 =~ s/^(.{$n}).{4}.*(.{$n})$/$1...$2/s; my $to = sprintf ("%02d %s -- %s (%s)", $i, $artist2, $name2, $year); $to =~ s@/@|@gsi; # characters that can't appear in file names $to =~ s@:@;@gsi; $to =~ s@\000@@gsi; $to .= ".mp3"; my $dest1 = "$dir_name1/$to"; my $dest2 = "$dir_name2/$to"; push @dest_files1, $dest1 if (! $twop); push @dest_files2, $dest2; if ($twop) { } elsif (-f $dest1) { print STDERR "$progname: $dest1 exists"; } elsif ($debug_p) { print STDERR "$progname: DEBUG: cp -p $from $dest1\n"; } else { safe_system ("cp", "-p", $from, $dest1); } if (-f $dest2) { print STDERR "$progname: $dest2 exists"; } elsif ($debug_p) { print STDERR "$progname: DEBUG: cp -p $from $dest2\n"; } else { safe_system ("cp", "-p", $from, $dest2); } $i++; } # Convert the MP3 files to WAV files, and run "normalize" on those. # if ($normalize_p) { # # Create all the WAVs.. # my @wavs = (); foreach my $file (@dest_files1) { my $wav = $file; $wav =~ s/\.mp3$/.wav/si; unlink $wav; push @wavs, $wav; my @cmd = ("lame", "--quiet", "--decode", $file, $wav); if ($verbose) { my $s = $file; $s =~ s@^.*/@@; print STDERR "$progname: converting \"$s\"...\n"; } $ENV{TERM} = 'dumb'; # LAME being lame if ($debug_p) { print STDERR "$progname: DEBUG: " . join (" ", @cmd) . "\n"; } else { safe_system (@cmd); error ("lame failed") unless (-f $file); } } # Normalize the WAV files... # my @cmd = ("normalize", "--no-progress", "--mix", @wavs); print STDERR "$progname: normalizing...\n" if ($verbose); if ($debug_p) { print STDERR "$progname: DEBUG: " . join (" ", @cmd) . "\n"; } else { safe_system (@cmd); } # Convert the normalized WAVs back to MP3s... # foreach my $file (@dest_files1) { my $wav = $file; $wav =~ s/\.mp3$/.wav/si; my @cmd = ("lame", "--quiet", "--noreplaygain", "-t", "--preset", "cbr", "128", "--resample", "44.1", # no-op unless src was <= 64kbps $wav, $file); if ($verbose) { my $s = $file; $s =~ s@^.*/@@; print STDERR "$progname: encoding \"$s\"...\n"; } $ENV{TERM} = 'dumb'; # LAME being lame if ($debug_p) { print STDERR "$progname: DEBUG: " . join (" ", @cmd) . "\n"; } else { unlink $file; safe_system (@cmd); unlink $wav; error ("lame failed") unless (-f $file); } } } else { # ! $normalize_p # # Create the 128kbps files in a single pass # foreach my $file (@dest_files1) { my $tmp = "$dir_name1/.tmp-" . sprintf ("%08x", rand(0xFFFFFFFF)); unlink $tmp; my @cmd = ("lame", "--quiet", "--mp3input", "--noreplaygain", "-t", "--preset", "cbr", "128", $file, $tmp); if ($verbose) { my $s = $file; $s =~ s@^.*/@@; print STDERR "$progname: converting \"$s\"...\n"; } $ENV{TERM} = 'dumb'; # LAME being lame if ($debug_p) { print STDERR "$progname: DEBUG: " . join (" ", @cmd) . "\n"; } else { safe_system (@cmd); error ("lame failed") unless (-f $tmp); unlink $file; safe_system ("mv", $tmp, $file); unlink $tmp; } } } # Create the high bitrate, fully tagged files # my $total = $i-1; $i = 1; my $png_data = `cat $ENV{HOME}/dna/webcast/mixtape.png`; foreach my $file (@dest_files2) { my $alb = $playlist_name; $alb =~ s/ .*$//s; $alb = "jwz mixtape $alb"; my $mp3 = MP3::Tag->new($file); $mp3->get_tags(); my $id3v1 = $mp3->{ID3v1} if exists $mp3->{ID3v1}; my $id3v2 = $mp3->{ID3v2} if exists $mp3->{ID3v2}; $id3v1->remove_tag() if $id3v1; # Delete the existing frames that we don't (explicitly) want. # my %allowed_frames = ( 'RVAD' => 1, # Relative volume adjustment 'TBPM' => 1, # Beats per minute 'TCOM' => 1, # Composer 'TIT2' => 1, # Title/songname/content description ("Name" in iTunes) 'TKEY' => 1, # Initial key 'TLEN' => 1, # Length (milliseconds) 'TPE1' => 1, # Lead performers/Soloists ("Artist" in iTunes) 'TSRC' => 1, # ISRC (international standard recording code) 'TYER' => 1, # Year 'USLT' => 1, # Unsychronized lyric/text transcription ); foreach my $frame (keys %{$id3v2->get_frame_ids()}) { my $frame2 = $frame; $frame2 =~ s/^(.+)\d\d$/$1/s; if ($allowed_frames{$frame2}) { print STDERR "$progname: $file: keeping $frame\n" if ($verbose > 2); } else { print STDERR "$progname: $file: deleting $frame\n" if ($verbose > 1); $id3v2->remove_frame($frame); } } $id3v2->add_frame("TALB", $alb) || error ("$file: add frame TALB failed"); $id3v2->add_frame("TCMP", 1) || error ("$file: add frame TCMP failed"); $id3v2->add_frame("TRCK", "$i/$total") || error ("$file: add frame TRCK failed"); $id3v2->add_frame("APIC", 0, "image/png", "\000", "", $png_data) || error ("$file: add frame APIC failed"); if ($debug_p) { print STDERR "$file: DEBUG: not writing tag\n"; } else { $id3v2->write_tag() || error ("$file: error writing tag"); } $i++; } } sub error($) { my ($err) = @_; print STDERR "$progname: $err\n"; exit 1; } sub usage() { print STDERR "usage: $progname [--verbose] mixtape-name\n"; exit 1; } sub main() { my $name; my $twop = 0; while ($#ARGV >= 0) { $_ = shift @ARGV; if ($_ eq "--verbose") { $verbose++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif ($_ eq "--debug") { $debug_p++; } elsif ($_ eq '-2') { $twop++; } elsif (m/^-./) { usage; } elsif (!defined($name)) { $name = $_; } else { usage; } } usage unless defined($name); mixtape_install ($name, $twop); } main(); exit 0;