#!/usr/bin/perl ############################################################################### # # fysh # # Copyright (C) 2007 Vincent Stehlé # # This is free software under GPL. See documentation and license in the # embedded doc. at the end of this file. # ################################################################################ use warnings; use strict; use File::Copy; use Fatal qw(:void open close read seek mkdir copy unlink); use Getopt::Long; use Pod::Usage; use Fcntl qw(:seek); ################################################################################ # # VERSION # ################################################################################ my $VERSION = '0.1'; ################################################################################ # # IPOD_MUSIC_DIR # ################################################################################ my $IPOD_MUSIC_DIR = "music"; ################################################################################ # # EXECUTABLES # ################################################################################ my @EXECUTABLES = qw(flac faac sox find python mount umount df grep sed cut); ################################################################################ # # ITUNESSD # # Description: # iTunesSD path, relative to iPod root. # ################################################################################ my $ITUNESSD = "iPod_Control/iTunes/iTunesSD"; ################################################################################ # # ITUNESSTATS # # Description: # iTunesStats path, relative to iPod root. # ################################################################################ my $ITUNESSTATS = "iPod_Control/iTunes/iTunesStats"; ################################################################################ # # REBUILD_DB # ################################################################################ my $REBUILD_DB = "rebuild_db.py"; ################################################################################ # # REBUILD_DB_URL # ################################################################################ my $REBUILD_DB_URL = "http://shuffle-db.sourceforge.net/"; ################################################################################ # # safe_system # ################################################################################ sub safe_system($) { my($c) = @_; my $r = system($c); die $c if ($r >> 8); } ################################################################################ # # flac_to_m4a # # Notes: # Shuffle cuts @20KHz. # ################################################################################ sub flac_to_m4a($$\%) { my($flac, $m4a, $opt) = @_; # Decode. my $tmp1 = "/tmp/tmp1-$$.wav"; safe_system("flac -d -o $tmp1 \"$flac\""); # Normalize. my $tmp2 = "/tmp/tmp2-$$.wav"; my $v = `sox $tmp1 -t raw /dev/null stat -v 2>&1`; chomp $v; print "Optimal volume: $v\n"; safe_system("sox -v $v $tmp1 $tmp2"); unlink($tmp1); # Encode. my $faac_opt = (exists $opt->{'faac-opt'}) ? $opt->{'faac-opt'} : ""; safe_system("faac -o \"$m4a\" -q 90 -c 19000 $faac_opt $tmp2"); unlink($tmp2); } ################################################################################ # # read_bytes # # Arguments: # size: in bytes. # ################################################################################ sub read_bytes($$) { my ($F, $size) = @_; my $x; read($F, $x, $size); return $x; } ################################################################################ # # read_n_bytes # ################################################################################ sub read_n_bytes($$;$) { my ($F, $n, $little_endian) = @_; # Defaults. $little_endian = 0 if !defined $little_endian; my $x = read_bytes($F, $n); my @t = unpack("C$n", $x); # Endianess. @t = reverse @t if !$little_endian; my $r = 0; my $cnt = 0; foreach my $v (@t){ $r += ($v << (8 * $cnt++)) } return $r; } ################################################################################ # # read_string # ################################################################################ sub read_string($$) { my ($F, $size) = @_; my $x = read_bytes($F, $size); # Cope with unicode's zeroes. my @t = unpack("S*", $x); $x = pack("C*", @t); return unpack("Z*", $x); } ################################################################################ # # read_itunessd # ################################################################################ sub read_itunessd(\%) { my ($opt) = @_; local *F; my $filename = "$opt->{'ipod-root'}/$ITUNESSD"; open(F, $filename); binmode F; # Header. my $num_songs = read_n_bytes(*F, 3); # print "num_songs: $num_songs\n"; seek(F, 3, SEEK_CUR); # Skip 3. my $header_size = read_n_bytes(*F, 3); # print "header_size: $header_size\n"; # Each entry. my @r; for my $song (0 .. ($num_songs - 1)){ # print "song $song\n"; seek(F, ($header_size + ($song * 558) + 33), SEEK_SET); my $filename = read_string(*F, 522); # print "filename $filename\n"; push(@r, $filename); } close(F); return @r; } ################################################################################ # # read_itunesstats # # Notes: # This one is little endian. # ################################################################################ sub read_itunesstats(\%) { my ($opt) = @_; local *F; my $filename = "$opt->{'ipod-root'}/$ITUNESSTATS"; open(F, $filename); binmode F; # Header. my $num_songs = read_n_bytes(*F, 3, 1); # print "num_songs: $num_songs\n"; # Each entry. my @r; for my $song (0 .. ($num_songs - 1)){ # print "song $song\n"; seek(F, (6 + ($song * 18) + 12), SEEK_SET); my $playcount = read_n_bytes(*F, 3, 1); # print "playcount $playcount\n"; my $skippedcount = read_n_bytes(*F, 3, 1); # print "skippedcount $skippedcount\n"; push(@r, {playcount => $playcount, skippedcount => $skippedcount}); } close(F); return @r; } ################################################################################ # # remove_previous_music # ################################################################################ sub remove_previous_music(\%) { my ($opt) = @_; my @sd = read_itunessd(%{$opt}); my @stats = read_itunesstats(%{$opt}); # Remove. for my $i (0 .. $#stats){ # print "${root}$sd[$i] played=$stats[$i]->{playcount}" # ." skipped=$stats[$i]->{skippedcount}\n"; if($stats[$i]->{playcount} || $stats[$i]->{skippedcount}){ my $f = "$opt->{'ipod-root'}$sd[$i]"; next if ! -e $f; print "Removing $f\n"; unlink($f); } } } ################################################################################ # # check_executables # ################################################################################ sub check_executables() { my $stop = 0; foreach my $e (@EXECUTABLES){ my $w = `which $e`; chomp $w; print STDERR "$e not in path.\n" if $w eq ""; } if($stop){ print STDERR "Stop."; exit 1; } } ################################################################################ # # Main. # ################################################################################ # Command line. # Shamelessly adapted from pod2usage man page. # We have default values. my %opt = ('ipod-root' => '/media/ipod'); ## Parse options and print usage if there is a syntax error, ## or if usage was explicitly requested. GetOptions(\%opt, 'faac-opt=s', 'handle-mount', 'help', 'ipod-root=s', 'man', 'music-dir=s', 'version') or pod2usage(2); pod2usage(1) if exists $opt{help}; pod2usage(-verbose => 2) if exists $opt{man}; # Handle version. if(exists $opt{'version'}){ print < END exit; } # Initialize random seed. srand(time); # Sanity. check_executables(); # Handle mount. system("mount $opt{'ipod-root'}") if exists $opt{'handle-mount'}; # Make room. remove_previous_music(%opt); # List flac. my @l; if(exists $opt{'music-dir'}){ @l = `find $opt{'music-dir'} -type f -name \*.flac`; chomp @l; # We sort the files, as we will use random selection thereafter. @l = sort(@l); } # Ensure directory exists on the iPod. my $music_dir = "$opt{'ipod-root'}/$IPOD_MUSIC_DIR"; if(! -e $music_dir){ print "Creating $music_dir\n"; mkdir($music_dir); } # Perform selection. my $tmp = "/tmp/tmp-$$.m4a"; my $cnt = 1; while(@l){ # Select one flac randomly. my $i = int(rand(@l)); # Remove it from list. my $flac = splice(@l, $i, 1); print "Selecting $flac\n"; # Compute dst name. my $dst = $flac; $dst =~ s/\.flac$/\.m4a/; $dst =~ s%$opt{'music-dir'}/%%; $dst =~ s%[/>\*]+%_%g; $dst = "$music_dir/$dst"; # Skip if exists. next if -f $dst; # Encode to aac. flac_to_m4a($flac, $tmp, %opt); # Get size of aac, in bytes. my(undef,undef,undef,undef,undef,undef,undef,$size, undef,undef,undef,undef,undef) = stat($tmp); # Compute size in 1K blocks. my $blocks = int(($size + 1023) >> 10); # Estimate free size on frame. my $free_blocks = `df |grep $opt{'ipod-root'} |sed 's/ \\+/ /g' |cut -d ' ' -f 4`; chomp($free_blocks); print "$free_blocks blocks remaining.\n"; # Enough free space remaining? if($free_blocks < $blocks){ print "iPod filled!\n"; last; } # Copy aac. copy($tmp, $dst); print "Copied to $dst\n"; $cnt++; } unlink($tmp); if(!@l){ print "Ran out of music!\n"; } $cnt--; print "Put $cnt pieces on iPod.\n"; # Rebuild db. my $rdb = "$opt{'ipod-root'}/$REBUILD_DB"; if(! -e $rdb){ print STDERR <> Pass additional options to faac. =item B<--handle-mount> Let fysh handle mount/umount. =item B<--help> Print a brief help message and exits. =item B<-i, --ipod-root=> Specify where the iPod shuffle is mounted. =item B<--man> Prints the manual page and exits. =item B<--music-dir=> Specify music dir to search for flac files. =item B<-v, --version> Prints version information and exits. =back =head1 DESCRIPTION fysh is meant to fill an iPod shuffle with music. It choses, encodes, transfers and registers the music into the iPod automatically. fysh is pronounced like "fish". =head1 REFERENCES File formats taken from: http://ipodlinux.org/ITunesDB. This script relies on the python database builder of http://shuffle-db.sourceforge.net/ =head1 AUTHOR Vincent Stehlé . =head1 COPYRIGHT Copyright (C) 2007 Vincent Stehlé. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =head1 TODO Some enhancements remain to be done to this program: =over 4 =item Blacklist undesired input files. =item Handle more in and out formats. =item wget rebuild_db.py when not found. =item Use GNU queue. Better: allow arbitrary wrapper for commands. =item Able to maintain a fifo/cache and fill it speculatively. =item Better error handling. =item Able to limit fill size. =back =cut