File Coverage

blib/lib/iPod/Squish.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package iPod::Squish;
4 1     1   28227 use Moose;
  0            
  0            
5              
6             our $VERSION = "0.02";
7              
8             use MooseX::Types::Path::Class;
9              
10             with qw(MooseX::LogDispatch);
11              
12             #use FFmpeg::Command;
13             #use Audio::File; # this dep fails if flac fails to build, so we use MP3::Info directly for now
14             use Number::Bytes::Human qw(format_bytes);
15             use MP3::Info;
16             use File::Temp;
17             #use Parallel::ForkManager;
18             use File::Which;
19              
20             has '+use_logger_singleton' => ( default => 1 );
21              
22             has use_lame => (
23             isa => "Bool",
24             is => "rw",
25             default => sub { defined which("lame") },
26             );
27              
28             has volume => (
29             isa => "Path::Class::Dir",
30             is => "ro",
31             required => 1,
32             coerce => 1,
33             );
34              
35             has music_dir => (
36             isa => "Path::Class::Dir",
37             is => "ro",
38             lazy => 1,
39             default => sub {
40             my $self = shift;
41             $self->volume->subdir( qw(iPod_Control Music) );
42             },
43             );
44              
45             has target_bitrate => (
46             isa => "Int",
47             is => "ro",
48             default => 128,
49             );
50              
51             has jobs => (
52             isa => "Int|Undef",
53             is => "ro",
54             default => 2,
55             );
56              
57             has fork_manager => (
58             is => "ro",
59             init_arg => undef,
60             lazy_build => 1,
61             );
62              
63             sub _build_fork_manager {
64             my $self = shift;
65              
66             my $jobs = $self->jobs;
67              
68             return unless $jobs or $jobs <= 1;
69              
70             require Parallel::ForkManager;
71             return Parallel::ForkManager->new( $jobs );
72             }
73              
74             has ffmpeg_output_options => (
75             isa => "HashRef",
76             is => "ro",
77             default => sub { {} },
78             auto_deref => 1,
79             );
80              
81             sub get_bitrate {
82             my ( $self, $file ) = @_;
83              
84             # for when we support more than just MP3s
85             #( Audio::File->new($file->stringify) || return 0 )->audio_properties->bitrate;
86              
87             my $info = get_mp3info( $file->stringify ) or return 0;
88              
89             return $info->{BITRATE} || 0;
90             }
91              
92             sub run {
93             my $self = shift;
94              
95             my @files;
96              
97             $self->music_dir->recurse( callback => sub {
98             my $file = shift;
99             push @files, $file if -f $file;
100             });
101              
102             $self->process_files(@files);
103             }
104              
105             sub process_files {
106             my ( $self, @files ) = @_;
107              
108             my @need_encoding;
109              
110             foreach my $i ( 0 .. $#files ) {
111             push @need_encoding, $files[$i] if $self->needs_encoding( $files[$i], $i + 1, scalar(@files) );
112             }
113              
114             foreach my $i ( 0 .. $#need_encoding ) {
115             $self->reencode_file($need_encoding[$i], $i + 1, scalar(@need_encoding));
116             }
117              
118             if ( my $pm = $self->fork_manager ) {
119             $pm->wait_all_children;
120             }
121              
122             return @need_encoding;
123             }
124              
125             sub needs_encoding {
126             my ( $self, $file, $n, $tot ) = @_;
127              
128             # itunes keep files in their original name while copying, this way we don't
129             # get a race condition
130             return unless $file->basename =~ /^[A-Z]{4}(?: \d+)?\.mp3$/;
131              
132             if ( ( my $bitrate = $self->get_bitrate($file) ) > $self->target_bitrate ) {
133             $self->logger->log( level => "info", message => "queueing $file ($n/$tot), bitrate is $bitrate" );
134             return 1;
135             } else {
136             $self->logger->log( level => "info", message => "skipping $file ($n/$tot), " . ( $bitrate ? "bitrate is $bitrate" : "error reading bitrate" ) );
137             return;
138             }
139             }
140              
141             sub reencode_file {
142             my ( $self, @args ) = @_;
143              
144             my $pm = $self->fork_manager;
145             $pm->start and return if $pm;
146              
147             $self->_reencode_file(@args);
148              
149             $pm->finish if $pm;
150             }
151              
152             sub _reencode_file {
153             my ( $self, $file, $n, $tot ) = @_;
154              
155             my $size = -s $file;
156              
157             $self->logger->log( level => "info", message => "encoding $file ($n/$tot)" );
158              
159             # make the tempfile at the TLD of the iPod so we can rename() later
160             my $tmp = File::Temp->new( UNLINK => 1, SUFFIX => ".mp3", DIR => $self->volume );
161              
162             if ( $self->run_encoder( $file->stringify, $tmp->filename ) ) {
163             my $new_size = -s $tmp->filename;
164             my $saved = $size - $new_size;
165              
166             $self->logger->log( level => "notice", message => sprintf "renaming %s, saved %s (%.2f%%) ($n/$tot)", $file, format_bytes($saved), ( $saved / $size ) * 100 );
167              
168             rename( $tmp->filename, $file )
169             or $self->logger->log( level => "error", message => "Can't rename $tmp to $file" );
170              
171             } elsif ( ( $? & 127 ) != 2 ) { # SIGINT
172             $self->logger->log( level => "error", message => "error in conversion of $file: $?" );
173             }
174             }
175              
176             sub run_encoder {
177             my ( $self, @args ) = @_;
178              
179             if ( $self->use_lame ) {
180             $self->run_lame(@args);
181             } else {
182             $self->run_ffmpeg(@args);
183             }
184             }
185              
186             sub run_lame {
187             my ( $self, $input, $output ) = @_;
188              
189             system ( qw(lame --silent -h --preset), $self->target_bitrate, $input, $output ) == 0;
190             }
191              
192             sub run_ffmpeg {
193             my ( $self, $input, $output ) = @_;
194              
195             require FFmpeg::Command;
196             my $cmd = FFmpeg::Command->new;
197              
198             $cmd->input_options({ file => $input });
199              
200             $cmd->output_options({
201             format => "mp3",
202             audio_codec => "mp3",
203             audio_bit_rate => $self->target_bitrate,
204             $self->ffmpeg_output_options,
205             file => $output
206             });
207              
208             $cmd->exec;
209             }
210              
211             __PACKAGE__
212              
213             __END__
214              
215             =pod
216              
217             =encoding utf8
218              
219             =head1 NAME
220              
221             iPod::Squish - Convert songs on an iPod in place using lame or
222             L<FFmpeg::Command>.
223              
224             =head1 SYNOPSIS
225              
226             use iPod::Squish;
227              
228             my $squisher = iPod::Squish->new(
229             volume => "/Volumes/iPod Name"
230             target_bitrate => 128,
231             );
232              
233             $squisher->run;
234              
235             =head1 DESCRIPTION
236              
237             This module uses F<lame> or L<FFmpeg::Command> to perform automatic conversion
238             of songs on an iPod after they've been synced.
239              
240             Since most headphones are too crappy to notice converting songs to a lower
241             bitrate is often convenient to save size.
242              
243             Only files with a bitrate over C<target_bitrate> will be converted.
244              
245             Currently only MP3 files will be converted and the output format is MP3 as
246             well. AAC support would be nice, see L</TODO>.
247              
248             A tip to sync more data than iTunes is willing is to do it in several steps by
249             using a smart playlist and limiting the number of songs in the playlist by the
250             number of free megabytes on the player. Then you can run the squishing script,
251             and repeat ad nauseum.
252              
253             =head1 ATTRIBUTES
254              
255             =over 4
256              
257             =item volume
258              
259             The mount point of the iPod you want to reencode.
260              
261             =item target_bitrate
262              
263             The bitrate to encode to.
264              
265             Only songs whose bitrate is higher than this will be encoded.
266              
267             =item use_lame
268              
269             Use the C<lame> command directly instead of L<FFmpeg::Command>.
270              
271             Defualts to true if C<lame> is in the path, because it's more flexible than
272             lame through ffmpeg.
273              
274             Note that using lame is generally slower for the same C<target_bitrate> because
275             of the C<-h> flag passed to lame.
276              
277             =item jobs
278              
279             The number of parallel lame instances to run. Defaults to 2. Useful for multi
280             processor or multi core machines.
281              
282             =back
283              
284             =head1 METHODS
285              
286             =over
287              
288             =item run
289              
290             Do the conversion by recursing through the iPod's music directory and running
291             C<process_file> for each file (possibly in parallel, see C<jobs>).
292              
293             =item process_file $file
294              
295             Attempt to convert the file, and if conversion succeeds replace the original
296             with the new version.
297              
298             The file will only be converted if its an MP3.
299              
300             =item reencode_file $file
301              
302             Does the actual encoding/move of the file.
303              
304             =back
305              
306             =head1 LOGGING
307              
308             This module uses L<MooseX::LogDispatch>, which in turn uses
309             L<Log::Dispatch::Config>. This allows you to control logging to your heart's
310             content. The default is to just print the messages to C<STDERR>.
311              
312             =head1 TODO
313              
314             =over 4
315              
316             =item VBR
317              
318             I'm not quite sure how to specify varible bitrate for C<ffmpeg>. Should look
319             into that.
320              
321             =item m4a
322              
323             Support C<m4a> type AAC files (I don't think ffmpeg allows this, but I'm not
324             quite sure). Encoding to AAC definitely is supported.
325              
326             =item format consolidation
327              
328             Check if an iPod will swallow files in a format different than the name/library
329             entry implies.
330              
331             If not, try to use rewrite library entries, as long as this doesn't affect
332             synchronization.
333              
334             Perhaps look at L<Mac::iPod::DB> for details.
335              
336             =head1 OSX agent integration
337              
338             Using an app called Lingon (L<http://lingon.sourceforge.net/>) you can easily
339             create an agent that will run every time a disk is mounted.
340              
341             My entry is:
342              
343             nice /usr/local/bin/perl -I /Users/nothingmuch/Perl/iPod-Squish/lib /Users/nothingmuch/Perl/iPod-Squish/script/isquish
344              
345             Because the script loops until no more songs are converted, and copying is
346             likely faster than encoding it should generally Just Workâ„¢ automatically.
347              
348             Make sure you have F<lame> or F<ffmpeg> in the path, and if you want lame to
349             STFU then set TERM to something as well.
350              
351             This can be done by using
352              
353             env TERM=xterm-color PATH=...
354              
355             to actually run the script.
356              
357             =head1 SEE ALSO
358              
359             L<FFmpeg::Command>, L<Audio::File>, L<Mac::iPod::DB>,
360              
361             =head1 VERSION CONTROL
362              
363             This module is maintained using Darcs. You can get the latest version from
364             L<http://nothingmuch.woobling.org/code>, and use C<darcs send> to commit
365             changes.
366              
367             =head1 AUTHOR
368              
369             Yuval Kogman E<lt>nothingmuch@woobling.orgE<gt>
370              
371             =head1 COPYRIGHT
372              
373             Copyright (c) 2008 Yuval Kogman. All rights reserved
374             This program is free software; you can redistribute
375             it and/or modify it under the same terms as Perl itself.
376              
377             =cut