#!/usr/bin/perl -w # Copyright © 2011-2012 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. # # Do a growl notification just after a song starts in iTunes, and just before # it ends, to display MTV-like music video credits. # # # Note: this requires a patch to the Growl 1.2.1 source to work properly. # Without this, Growl can't display its notifications above iTunes playing # videos in a full-screen window. In the file # "Plugins/Displays/MusicVideo/GrowlMusicVideoWindowController.m" # (or whichever display mode you are using) change the line # # [panel setLevel:NSStatusWindowLevel]; # to # [panel setLevel:CGShieldingWindowLevel()]; # # and recompile. Then replace "/Library/PreferencePanes/Growl.prefPane/ # Contents/Resources/GrowlHelperApp.app/Contents/PlugIns/MusicVideo.growlView" # with the new build and restart Growl. # # # Created: 9-May-2011. require 5; #use diagnostics; use strict; my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.9 $ }; $version =~ s/^[^\d]+([\d.]+).*/$1/; my $verbose = 0; $ENV{PATH} = "/opt/local/bin:/usr/local/bin:" . $ENV{PATH}; # Try to notify N seconds after the song starts, and N seconds before it ends. my $start_distance = 6; my $end_distance = 10; # Image to use in the Growl notification, or undef. my $logo = $ENV{HOME} . '/Pictures/dnapizza-userpic-1.png'; sub timestr($) { my ($x) = @_; $x = sprintf("%.2f", $x); if ($x >= 60) { my ($frac) = ($x =~ m/\.(\d+)$/si); $x = sprintf ("%d:%02d", int($x/60), int($x%60)); $x .= ".$frac" if ($frac); } $x =~ s/0+$//s if ($x =~ m/\./s); $x =~ s/\.$//s; return $x; } sub itunes_running_p() { `killall -s -0 iTunes 2>&1`; my $exit = $? >> 8; return ($exit == 0); } sub growl_itunes() { $logo = undef unless -f $logo; my $script = ("tell application \"iTunes\"\n" . " set P to player position\n" . " tell current track\n" . " " . join (" & \"\\t\" & ", ("artist", "name", "year", "video kind", "P", "start", "finish", "duration")) . " & \"\\n\"\n" . " end tell\n" . "end tell\n"); my $state = 'intro'; my $last_track = ''; while (1) { if (!itunes_running_p()) { # If iTunes isn't running, don't run any Applescript, because # that would have the side-effect of launching iTunes! That's # very annoying because it will cancel a "Shutdown". # print STDERR "$progname: iTunes not running\n" if ($verbose); sleep (5); next; } my $result = `osascript -e '$script'`; $result =~ s/\n+$//s; print STDERR "$progname: <= $result\n" if ($verbose > 3); my ($artist, $title, $year, $kind, $pos, $start, $length, $length2) = split (/\t/, $result); if (! $title) { # iTunes not responding, retry right away print STDERR "$progname: itunes not responding, retrying...\n"; sleep (1); next; } foreach ($start, $length, $length2) { $_ = sprintf("%.2f", $_); } # iTunes 10.2.2 ignores "Stop Time" on videos! Fix it! # if ($pos > $length) { print STDERR "$progname: iTunes ignored Stop Time! Skipping " . timestr($length2 - $length) . "\n" if ($verbose); system ("osascript", "-e", "tell application \"iTunes\" to next track"); sleep (2); next; } my $track = "$artist\t$title"; my $delay = 0; my $ostate = $state; if ($track ne $last_track) { # forces state reset $state = 'intro'; $ostate = 'track_change'; $last_track = $track; } if ($state eq 'intro') { # haven't notified yet if ($pos >= $start + $start_distance) { # notify now $state = 'msg1'; $delay = $length - $pos - $end_distance; # time until 'msg2' } else { # don't notify yet $delay = $start_distance - $pos; # time until 'msg1' } } elsif ($state eq 'msg1') { # have notified once if ($pos >= $length - $end_distance - 1) { # notify again $state = 'msg2'; $delay = $length - $pos + $start_distance; # time until 'msg1' # Kludge for the "Stop Time" workaround, above: stop at desired # end-of-track so that we can advance at the right time. $delay -= $start_distance - 1 if ($length != $length2); } else { # don't notify yet $delay = $length - $pos - $end_distance; # time until 'msg2' } } elsif ($state eq 'msg2') { # notified twice $delay = $length - $pos + $start_distance; # time until 'msg1' } else { error ("unknown state: $state"); } print STDERR "$progname: state " . ($ostate eq $state ? $state : "$ostate -> $state") . ", next state in " . timestr($delay) . "\n" if ($verbose); if ($delay <= 0 && $verbose) { print STDERR "$progname: OOPS: $pos, $start, $length, $length2\n"; } print STDERR "$progname: state $state, kind $kind," . " len " . timestr($length) . "," . " pos " . timestr($pos) . " (" . int(100 * $pos / $length) . "%)\n" if ($verbose > 1); if ($state ne $ostate && # state changed ($state eq 'msg1' || # to a notification state $state eq 'msg2') && $kind =~ m/video|movie/si && # and it's a video $length > 45) { # and it's not an interstitial my $t = $title; $t .= " ($year)" if ($year && $title !~ m/\b$year\b/s); my @cmd = ("growlnotify", "-t", $artist, "--message", $t); push @cmd, ("--image", $logo) if $logo; print STDERR "$progname: exec: " . join (" ", map { local $_ = $_; s/^(.*[^-a-z\d].*)$/"$1"/si; $_; } @cmd) . "\n" if ($verbose); system @cmd; } $delay = 20 if ($delay > 20); # Notice soon if "Next" was hit. $delay = 0.25 if ($delay < 0.25); print STDERR "$progname: sleep " . timestr($delay) . "\n" if ($verbose > 1); #sleep ($delay); select (undef, undef, undef, $delay); } } sub error($) { my ($err) = @_; print STDERR "$progname: $err\n"; exit 1; } sub usage() { print STDERR "usage: $progname [--verbose]\n"; exit 1; } sub main() { while ($#ARGV >= 0) { $_ = shift @ARGV; if (m/^--?verbose$/) { $verbose++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif (m/^-./) { usage; } else { usage; } } growl_itunes(); } main(); exit 0;