if its too loud, turn it down

Thursday, December 31, 2009

Applescript to apply mp3gain to iTunes playlists from the Script Menu

First off, if you use iTunes "Sound Check" and like it you may not need this. I disabled it because with iTunes 9, because I had alot of problems importing large mp3 files. It would often hang when determing the mp3's volume. Also, I was never able to tell the difference one way or the other. Playlists with songs from different albums still sounded un-even when Sound Check was enabled. If you love Sound Check and don't have any problems with it, this tip is not for you.

There are a couple other options for "normalizing" MP3 volume on OS X. One is iVolume which I've never tried. I read good things about it...but it costs money. Another is what I am going to focus on here: MacMP3Gain which is a port of the open source command-line program mp3gain for OS X (they also added a GUI).

mp3gain does more than just normalization, it does analysis to determine how quiet or loud an mp3 will sound to the human ear. If you really want to get technical, mp3gain does its analysis based on the Replay Gain algorithm. mp3gain applies lossless adjustment - it does not re-encode the mp3. However, MacMP3Gain offers this caveat about mp3gain, "MacMP3Gain modifies MP3 and unprotected AAC files with no provision provided to undo the changes." I haven't had any trouble after using extensively.

The MacMP3Gain application does have a GUI, which allows you to process by folder or by iTunes playlist. However, in the spirit of efficiency, I wanted a way to be able to normalize playlists right from iTunes. So I wrote this script which can be run right from the iTunes script menu. The other advantages of this script vs. the MacMP3Gain GUI is that it gives you a proper progress bar (important because it can take a long time to process), and it shows you the output when its done (so you can see exactly what changes were made to each file).

Prerequisites

MacMP3Gain - Install MacMP3Gain on your Mac. To use this applescript, you'll need to create a symlink from the command-line binary to somewhere in your path. This can be done with this command:
sudo ln -s /Applications/MacMP3Gain.app/Contents/Resources/aacgain /usr/bin/mp3gain
You can now use mp3gain on the command line. For a complete list of switches, open up Terminal and type:
mp3gain -?
mp3gain is also available via MacPorts, if you use that. Though at the time of writing, the newest version was 1.7.0 while the version rolled into the Intel MacMP3Gain is 1.8.0. To install via ports do this:
sudo port install aacgain
sudo ln -s /opt/local/bin/aacgain /usr/bin/mp3gain
Note: if you don't want to use any symlinks then just update the "mp3gain" reference in the applescript to point to the full path of your installed aacgain program.

BP Progress Bar - mp3gain takes roughly 30 seconds to process each mp3. Therefore, it can take a while to process an entire playlist. So, I've configured this script with a handy progress indicator. Applescript has no "native" progress indicator method, but you can access an external app to do this for you. Download BP Progress Bar (download link), unzip it and mount the disk image. Then, copy the app "BP Progress Bar" to your "Applications" folder, and the "BP Progress Bar Controller.scpt" to your Scripts folder /Users/YOU/Library/Scripts/ (create if it doesn't exist) and you're all good to go!

Please Note: The first time you run this script, you might get the rainbow wheel for 10-20 seconds, and you'll probably get the "BP Progress Bar was downloaded from the internet" security warning. Both of these things happen only the first time.

Now, copy the script below to Script Editor, and save it as "normalize_playlist.scpt" in your iTunes scripts directory /Users/YOU/Library/iTunes/Scripts/ (create if it doesn't exist). This will allow you to run it from the iTunes script menu like below:

iTunes Script Menu

Upon launch, you're prompted to enter a playlist to normalize.

Enter Playlist

As long as you enter a good one, you should see a progress bar come up.

Progress Bar

Upon completion you can view the log, which is written to /tmp/mp3gain_output.log.

MP3Gain Log

Enjoy!

(* 

Normalize Playlist

Accepts an itunes playlist as text input,
and normalizes all mp3 files on the playlist
with mp3gain -r (mp3gain itself decides how 
best to normalize).

Prerequisities:

* mp3gain on command-line
* http://homepage.mac.com/beryrinaldo/AudioTron/MacMP3Gain/
* BP Progress Bar
* http://scriptbuilders.net/files/bpprogressbar1.0.html

*)

--Ask the use for the playlist
set myList to the text returned of (display dialog "Enter playlist to normalize " default answer "")

--exit if they didn't enter anyting
if the myList is "" then
 display dialog "No playlist entered" giving up after 2
 return
end if

--make sure itunes is running
--SHOULD BE if it's run from the itunes script menu
--but it could be executed directly
set itunesOK to my itunes_is_running()
if itunesOK is false then
 tell application "iTunes"
  activate
 end tell
end if

tell application "iTunes"
 set oldfi to fixed indexing
 set fixed indexing to true
 
 --see if the playlist exists
 if exists user playlist myList then
  --do nothing for now
 else
  --show error if the playlist doesn't exist
  display dialog "Playlist does not exist" giving up after 2
  return
 end if
 set currentList to playlist myList
 
 --initialize progress bar
 set ProgressBar to load script alias (((path to scripts folder) as text) & "BP Progress Bar Controller.scpt")
 set myTitle to "Normalizing " & myList & " - may take several minutes"
 tell ProgressBar to initialize(myTitle) -- title of progress bar
 -- Start of Script to use ProgressBar Controller
 tell ProgressBar
  barberPole(true)
  setStatusTop to "Initializing Volume Adjustment"
  setStatusBottom to ""
 end tell
 
 --get the number of items on the playlist
 set eop to index of last track of currentList
 
 -- Stop the barber pole, set up for the progress bar
 tell ProgressBar
  barberPole(false)
  setMax to eop -- to match the items to be processed below
  setStatusTop to "Examining playlist"
 end tell
 
 --add a little progress so it doesn't start at 0
 tell ProgressBar to increase by 1
 
 with timeout of 10800 seconds --avoid "event timed out" error
  
  --delete the logfile if it already exists
  do shell script "if [ -e /tmp/mp3gain_output.log ]; then rm -f /tmp/mp3gain_output.log; fi;"
  
  repeat with i from 1 to eop
   
   --write current track to log
   do shell script "echo \"------------ Track " & i & " of " & eop & " ------------\" >> /tmp/mp3gain_output.log"
   
   --get the mac path to the mp3 file, name of the track, and extension
   set i_location to (get location of track i of currentList)
   set i_name to (get name of track i of currentList)
   set theFileInfo to info for i_location
   set ext to name extension of theFileInfo as string
   
   --only do this if it's an mp3
   if ext is "mp3" then
    
    --convert mac path to POSIX path, quote it so we
    --can use it on the cmd line
    set mypath to POSIX path of i_location
    set posixpath to quoted form of mypath
    
    --create our command
    --mp3gain is CPU-intensive, so pass thru nice
    --write output of mp3gain to log
    --this will allow us to report on what changes were made
    set myCmd to "nice mp3gain -r -k -c -q " & posixpath & " >> /tmp/mp3gain_output.log"
    
    --update progress window with status
    tell ProgressBar
     setStatusTop to "Processing file " & i & " of " & eop & " : " & i_name
     setStatusBottom to "Full path: " & mypath
    end tell
    
    --execute the shell command
    do shell script myCmd
   else
    --if track is not an mp3, don't process it
    do shell script "echo \"Track " & posixpath & "is not an mp3...not processing\" >> /tmp/mp3gain_output.log"
    
   end if --end if for is an mp3
   
   tell ProgressBar to increase by 1
   
  end repeat
 end timeout
 
 set fixed indexing to oldfi
end tell

tell ProgressBar to quit

--tell them we're done and ask if they want to see log
set seeLog to (display dialog ¬
 "Done. Would you like to see the log? " with title ¬
 "Normalization Complete" buttons {"Yes", "No"} ¬
 default button "Yes")
set button_name to button returned of seeLog
if button_name is "Yes" then
 --open log in textedit
 tell application "TextEdit"
  activate
  open "/tmp/mp3gain_output.log"
 end tell
end if

--be nice and clean up
do shell script "if [ -e /tmp/mp3gain_output.log ]; then rm -f /tmp/mp3gain_output.log; fi;"

return

--subroutine checks if itunes is running
on itunes_is_running()
 tell application "System Events" to return (exists process "iTunes")
end itunes_is_running

Thursday, December 17, 2009

Honky Tonk Christmas - Pinto Bennett - Lyrics and Chords

One of my all-time favorite Christmas songs.

                                     F
Merry Christmas darlin'.  I see I've won the football pool.

C                                         G
I'll buy everyone a drink....that doesn't drool.

C
Then double mine, and triple that!

    F                        C
And maybe I'll believe, it's really great to be here

                G         C             G climb to C         
...with all you drunks on Christmas Eve.

C                                        F
BUT...I hung up my Christmas sock, and I sang a Christmas chant

      C                               G                 climb to C
asked Santa for a college girl, and a liver transplant

C                                            F
But the wind still blows in Wyomin', and ol' Sam still smells like beer...

     C                 G               C      G climb to C  
I'll have a Honky Tonk Christmas, this year.  (I'll have a...)


[chorus]

C           F               C             C                                 G       climb to C
Hon-ky Tonk Christmas, this yeeeeeeeear.  Hit every bar with parkin' in the rear.

C                                  F
And go home with a six pack, and a bottle of good cheer...
           
           C          G               C       G climb to C
and have a honky tonk Christmas, this year.

[break - chorus chords]
C F C C G C F C G C G climb to C

C                          F                 C
The sun sun shines thru my room, and o'er my word.

C                             G       climb to C
My chronometer, reads January third.

   C          F             C                 F
My honky tonk Christmas has passed, and now I fear...

            C          G          C      G climb to C  
I'll have a honky tonk year, this year.  (I'll have a...)

[chorus]

C           F               C             C                                 G        climb to C
Hon-ky Tonk Christmas, this yeeeeeeeear.  Hit every bar with parkin' in the rear.

C                                  F
And go home with a six pack, and a bottle of good cheer...
           
           C          G               C       G to C finish
and have a honky tonk Christmas, this year.

Thank you, thank you thank you, lovers of fine and classical music!

Tuesday, November 3, 2009

My experience with a Dell Mini 10v and Snow Leopard

Hackbook

I work on a MacBook Pro 2.33ghz laptop with 3G of RAM and it has suited me fine for almost three years now. The only thing that worries me is that if I lost it, it was stolen or it just plain died, I'd be S.O.L. I'd have no way to get work done until I get a new computer set up. But I didn't want to plunk down the however many thousands for another mac just for peace of mind. Enter the netbook.

When I discovered that OS X could be installed on one of the more popular netbooks, the Dell Mini, I immediately bought the 10v. The Dell Mini is one of the few PCs out there that OS X can be installed on almost without fail, and with all the functions and devices working. And the best thing about them, they start at $250.

When I got it though, I was disappointed. The whole thing is just cheap...cheap, cheap, cheap. You truly do get what you pay for. And yes I DO realize I didn't pay much. The keyboard is tiny, the trackpad is awful and the worst part is, the max screen res is 1024x600. At best, this thing is a toy. The saving grace is that it has 3 USB ports and VGA out, so you can hook up an external keyboard, mouse and monitor and it becomes somewhat usable. So ultimately, even though I feel this computer is not really viable on its own...it does suit my purpose for it as my backup computer.

Upgrading the Hardware

The 10v comes with a 1.6ghz Intel Atom N270 processor, 1GB of RAM and either a small solid state drive or a 160GB 5400 RPM SATA drive. I opted for the latter, because the SSD drives were just too small. I should have gone with an SSD drive anyway, as I later decided that a 5400rpm drive was going to be too slow. So I bought a Hitachi 360GB 7200rpm OEM drive. I also bought a cheap 2GB stick of RAM. I wanted to make this thing as fast as possible despite its diminutive size.

Before doing any OS installation, I set out to replace the RAM and HD. Dell didn't make it easy to replace the RAM. Its buried inside, underneath the motherboard. You basically have to disassemble the entire computer to replace it. Fortunately, I found the video below. The guy did a very nice job...without his help I never would haven gotten it done.



Downgrading the BIOS

For some reason, the BIOS that ships with the 10v (A06) will not support(/allow?) OS X to be installed, so I had to downgrade the BIOS to A04. I followed this guide and was able to accomplish that without too much difficulty (you need access to a windows computer, I used winXP/Parallels on my mac, and a thumb drive). NOTE: when downloading the BIOS from the Dell site, be sure you get the one that says "DOS version".

Installing Snow Leopard

To do this, I followed these instructions precisely. It is the best guide on the internet for installing Snow Leopard on a 10v I could find. The only problems I encountered were that the computer would hang on the "Dell Inspiron" bootup screen for a long time (5 min) and then finally boot...until I realized that for some reason it doesn't like my external DVD-R drive. So, I unplugged that at boot-time and it boots fine. (The DVD drive works fine when I plug it in after the computer boots).

Software Installation

Once you're in the OS, software installs like it normally would. There were only two issues I have come across:

Virtual Machines

Out of the 3 main virtual machine software providers (Parallels, VirtualBox, VMware), only VMware's "Fusion Desktop for Mac" will work. They all install OK, but VirtualBox is incredibly unstable, and Parallels just shows an error message to the effect of "You can't use this version of the application Parallels Desktop with this verson of Mac OS X". I saw some posts that seemed to say that it's because the hardware doesn't support "virtualization" and that Parallels 3.0 works. I tried that, and it didn't (same error). Why VMware works and the others don't? I have no idea, but it's OK with me. I very rarely use windows anyway.

Macports/X11

The other problem I had was that some Macports software would not install. This is more of a Snow Leopard issue than a 10v issue though. Specifically, mplayer would not install via macports. However, I found this very nice GUI version of mplayer for Snow Leopard which has the command-line version rolled into it. Just create a symlink to it like this:
sudo ln -s /Applications/MPlayer\ OSX\ Extended.app/Contents/Resources/External_Binaries/mplayer.app/Contents/MacOS/mplayer /usr/bin/mplayer
UPDATE: as of 11/5/09 Macports mplayer is not supported in Snow Leopard, but mplayer-devel is. My first try at installing it failed, but I tried the following today and it compiled, installed and works:
sudo port -v install mplayer-devel +binary_codecs +dts +dv +x264 +faac -glx +theora +twolame +xvid
The other issue I had was with gnucash, which depends on the gnome libraries and X11 (X11 is in the "Optional Installs - Developer Tools" on the Snow Leopard DVD). It compiled and installed fine, but wouldn't run. It just kept spitting this error out:
Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
The fix is to first make sure dbus is installed. Then run this command as the user who is running the X11 app:
launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist
Bad, bad, really bad news...

If this rumor is true then I will forever be stuck with 10.6.1, which isn't good. Pretty lame, in fact.

UPDATE: Some very well-mannered gentlemen on the forums at the myDellMini site kindly let me know that the rumor may not be true.

Pros & Cons / Final thoughts

The good...
  • Quite portable
  • Cheap
  • Reasonably powerful given its size and price
  • VGA out, 3 USB ports
  • Long battery life, 6-cell battery available with 7+ hours reported
  • Works flawlessly with Snow Leopard - everything supported!
The bad...
  • 1024 X 600 max screen resolution. Nuts.
  • Display has a blue-ish cast
  • Trackpad is so poor it's un-usable
  • Keyboard is compacted, issues typing on it
  • No hardware virtualization
  • Further OS updates will be impossible
  • Very difficult to upgrade hard drive and memory
  • No holes for the VGA cable to screw into, keeps falling out
This is a fun little project, but in light of the fact you have to use an external keyboard/mouse/display to operate this thing (making its portability basically obsolete), and the fact that I probably won't be able to upgrade the OS ever again...if I could do it again I'd probably build a cheap hackintosh desktop box with a processor that will definitely continue to be supported - like a core 2 duo. It should cost about the same amount of money - ok, maybe a little more, but not nearly as much as a new mac - and would be a much faster computer.

...I find it somewhat ironic that Dell, notorious for making laptops too large, is now the leading manufacturer of the netbook, a laptop that's way too small. IN MY HUMBLE OPINION!!

Thursday, October 15, 2009

Use Perl and Expect to auto-connect, and maintain connection, to a proxy

There are a number of reason to use a proxy, security and privacy being the two biggest for me. I use Cotse.net and am very happy with it. There is one downside to Cotse (and alot of similar services I suppose) is that you must manually connect to the proxy server, using something like SSH Tunnel Manager. Kind of a pain.

So I wrote a script that manages the connection using perl and expect. It runs as a cronjob to always make sure you're connected to the proxy when an internet connection is available.

Installation
  1. Step 1: Install the Expect.pm perl module. As root, type this:
    # perl -MCPAN -e 'install Expect'
    
  2. Step 2: Edit this script and put in your /Users/user/bin/ directory as "proxy.pl".
    # check if we are connected to the proxy.  If not, connect.
    my $pid = &checkAlive($command);
    if (!$pid) {
    
     # check if we even have internet before trying to connect to proxy
     my $result = &checkInternet($ext_host,$timeout);
     if ($result > 0) {
      print "Not connected. Connecting to $command\n";
      &connect($password,$command,$timeout);
     } else {
      print "No internet available.\n";
     }
     
    } else {
     # we do have internet, and are connected toi proxy.
     print "connected to $command\n";
    }
    
    sub connect {
     # connect to the proxy and login using expect
     
     my ($password,$command,$timeout) = @_;
     
     # Create the Expect object
     my $exp = Expect->spawn($command) or die "Cannot spawn ssh command\n";
     $exp->expect($timeout,
      ["Password Authentication"],
      ["Are you sure you want to continue connecting", sub {my $self = shift; $self->send("yes\n");}]
      );
     # answer the password
     $exp->expect($timeout,
      [ qr/Password:/ => sub { my $exp = shift; $exp->send("$password\n"); exp_continue; } ],
      [ qr/Password:/ => sub { my $exp = shift; $exp->send("$password\n"); } ], 
      );
     # wait forever for nothing...basically, stay connected
     $exp->expect(undef);
     
    } # end sub connect
    
    sub checkInternet {
     # check if the internet is up by pinging external host
     # sends back packets received, so if the return val
     # is > 0 then we have internet
    
     my ($host,$timeout) = @_;
     my $packets = 0;
     
     my $pingcmd = "/sbin/ping -q -t $timeout -c 1 $ext_host";
     #print "checking for running program with $pingcmd\n";
     my @raw = `$pingcmd`;
     chomp(@raw);
     foreach my $line (@raw) {
      if ($line =~ /([0-9]{0,2}) packets received/) {
       $packets = $1;
      }
     } # end foreach
     return ($packets);
     
    } # end sub checkInternet
    
    sub checkAlive {
     # gets the PID of the process that matches a SINGLE criteria 
    
     my ($text) = shift;
     my $return_pid = "";
     
     my $pidgetter = "/bin/ps ax -o pid,command | grep '$text' | grep -v 'grep' | grep -v 'sh -c'";
     #print "checking for running program with $pidgetter\n";
     my @raw_pids = `$pidgetter`;
     chomp(@raw_pids);
     
     foreach my $raw_pid (@raw_pids) {
      $raw_pid =~ s/^\s+//;
      $raw_pid =~ /^(\d+)\s/;
      $raw_pid = $1;
      my $pid = stripNonNumber($raw_pid);
      my $exists = kill("0",$pid);
      if ($exists) {
       my $message = "checking for any program operating on $text. I found one with pid ${pid}. ";
       $return_pid = $pid
      }
     } # end foreach
     
     return ($return_pid);
    
    } # end sub checkAlive
    
    sub stripNonNumber {
    
     my($text) = shift;
     $text =~ s/([^0-9])//g;
     return ($text);
    
    } # end sub strip non number
    
    Then type this:
    cd ~/bin ; chmod 755 proxy.pl;
    
  3. Step 3: Add to cron. Type
    crontab -e
    . Add this line (you will need to edit where it says "user"):
    * * * * * /Users/user/bin/proxy.pl  > /dev/null 2>&1
    

Sunday, October 11, 2009

Use Thunderbird Extension "Send Later" (or "at") to email requests to time-shifted programs

OK, here's a tip you're unlikely to use. See if you fit these criteria...
  • You time-shift internet radio programs.
  • You sometimes want to communicate with the host/dj. For example, to make a music request
  • The program has a non-show-specific email address. For example, dj *at* kexp.org (for making requests to the current DJ).
  • You use Mozilla Thunderbird as your e-mail client.
  • You leave your computer on all the time, with your e-mail client open.
...still with me? I've got a solution for you! There's a great extension for Thunderbird called "Send Later".

Basically, it allows you to schedule emails to send at precise date/time. Here's what you do:
  1. Right-click this link and "Save link as" and save it to your desktop.
  2. In Tunderbird, go Tools -> Add-ons and click the "Install..." button. Choose the .xpi file you saved to your desktop. You will need to re-start Thunderbird.
  3. Once installed, compose your e-mail message as you would any e-mail. Instead of sending, choose File -> Send Later. That will bring up this dialog: Send Later Dialog
  4. Schedule your send using the date/time menus at the top. Then click "Send Later at specified time". I'd disregard all other options.
Then, listen to your time-shifted program for your request! If you need to edit the e-mail before the schedule send time, look for it in your "Drafts" folder. Just remember to choose File -> Send Later again instead of clicking "send". WARNING as of Send Later version 1.2.0.0 there is a strange, annoying bug that affects replying to or forwarding messages that have large attachments. Took me ages to figure out that the Send Later extension was causing it. Not a deal-breaker, but the following option may be better...

OR, be an ultra-nerd and use the "at" command...

For those nerdily inured to Mac OS X's UNIX features, you can accomplish the same feat as outlined above using "at".

First, you need to enable the
atrun
utility. It runs commands scheduled with
at
, but is disabled by default.
su
to root and run this command:
launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist
And if you haven't configured postfix to send outgoing mails, you'll need to do that. Check out this great tip on how to set up postfix to relay through GMail.

Alright, so to schedule an email to be sent, open up a terminal and first type the date/time (in POSIX format) you want it to be sent like this:
$ at -t 200910112232
You will then enter input mode. Enter your commands here like this:
mail -s "Music Request" [ENTER TO EMAIL HERE]
Dear DJ,
Last night you saved my life.  Can you
please play, "Crazy Horses" by The
Osmonds?  Thanks so much!
Love,
Me
Then hit enter once (blank line), then CTRL-d to exit input mode. Your mail is now scheduled! To see scheduled jobs, enter the 'atq' command.

Tuesday, September 22, 2009

Recording "The Moth Radio Hour" Pilot Episodes

As you know, "The Moth" is a jammin' sweet podcast. It's bringing back the lost art of storytelling. The only downside is that the podcasts are not that frequent. For high-volume podcast listeners like myself, it leaves you wanting more. Enter The Moth Radio Hour! An hour of well-curated stories selected from their years of archives. It's currently in "pilot" mode, being tested on some of the larger Public Radio Stations around the country: Great! So just look at the program listings for those stations and set your favorite time-shifting program to record. Only problems are the a) it's a pilot so it may not be in that spot forever (or at all) and b) you've missed the first several episodes.

Though it's not podcasted (yet), there is a way to listen to the first several episodes of TMRH. See http://www.prx.org/the-moth. There are flash players embedded under each episode summary, allowing you to stream the audio right from the site.

The downside here is that you can only listen from your computer. However, there are a couple methods you can employ to "rip" the stream to mp3, so you can take it with you.

Method One: WireTap

WireTap is the preferred method because it has the most flexibility...it's basically built for this sort of thing. Unfortunately, it does cost (but in my opinion, is worth it). See this article on how to set up a recording session with Wiretap.

Method Two: Soundflower and Audacity

This one's a little more complex, and you will hear the audio playing as it's recorded (WireTap allows you to mute the source playing the stream, so you can still use your speakers to listen to music or whatever). So you'd either have to mute your speakers, or record when you're not at the computer. The upside, is that the tools are free: audacity and soundflower.

Rather than try to explain it all myself, see this article on how to set it up.

Both methods require you to manually press "record" on the recorder (WireTap or Audacity) and then "play" on the TMRH episode flash player. When it's done (53 minutes later), you press stop on your recorder and then save as an MP3 and voila! You have your TMRH "podcast".

For super-geeks, you can also play the stream from VLC. I tried, and failed, to get mplayer to load it from the command line (my preferred method of recording). But VLC will do. The links to the media for the first several TMRH episodes are:
http://themothopen.prx.org.s3.amazonaws.com/MothHour1_broadcast.mp3?AWSAccessKeyId=11RCNQMECKHP4QK3CP02%26Expires=1406388439%26Signature=aSiteObGTnrY3LEGp3He8pPxBGI%3D
http://themothopen.prx.org.s3.amazonaws.com/MothHour2_broadcast.mp3?AWSAccessKeyId=11RCNQMECKHP4QK3CP02%26Expires=1406388420%26Signature=lTdGgKFB/8UiHGtEpyO6IgBnUYk%3D
http://themothopen.prx.org.s3.amazonaws.com/MothHour3Final_broadcast.mp3?AWSAccessKeyId=11RCNQMECKHP4QK3CP02%26Expires=1406999085%26Signature=fewikyPmyfjs8sAnUiveUA1qfrs%3D
http://themothopen.prx.org.s3.amazonaws.com/MothFinalHour4_broadcast.mp3?AWSAccessKeyId=11RCNQMECKHP4QK3CP02%26Expires=1406388475%26Signature=fLqPoe5HUIn5mCBwsGvfAhQ30Gw%3D
http://themothopen.prx.org.s3.amazonaws.com/MothFinalHour5a_broadcast.mp3?AWSAccessKeyId=11RCNQMECKHP4QK3CP02%26Expires=1406388977%26Signature=J7W1JthdNyNK7ZRKQQlbT3wdDTs%3D
Open VLC, press "play" to get a prompt for a MRL and enter one of the URLs above. When you play the episode you will get errors. Dismiss them and hit play again and the episode will start playing. Record or listen. Enjoy!

Wednesday, August 19, 2009

Applescript to add selected tracks to a playlist

I created this because I am clumsy with the pointer. After selecting a large number of tracks in iTunes with command-click I inevitably accidentally click and lose my selections. With this, I just hit a hotkey and get prompted for the playlist I want to add the selections to. If the list doesn't exist, it will be created.

This is just one more way OF MANY to add tracks to a playlist. I am a huge efficiency guy, so I set this script to be run with a Quicksilver keyboard hotkey trigger.

Here's the script:
--make sure itunes is running
set itunesOK to my itunes_is_running()
set myMessage to ""

if itunesOK then
 tell application "iTunes"
  
  -- if no tracks selected, exit with message
  if selection is {} then
   set myMessage to "No tracks selected"
   set myReturn to my growlMessage(myMessage)
   return
  end if
  
  --display prompt for playlist
  set myList to the text returned of (display dialog "Enter playlist to add selected tracks to" default answer "")
  
  --exit if they didn't enter anyting
  if the myList is "" then return
  
  set oldfi to fixed indexing
  set fixed indexing to true
  set newCount to 0
  set existsCount to 0
  set deadCount to 0
  
  --see if the playlist exists
  if exists user playlist myList then
   --do nothing for now
  else
   make new user playlist with properties {name:myList}
  end if
  set currentList to playlist myList
  
  --see if the track exists on the playlist
  set currentIDs to {}
  try
   if exists (track 1 of currentList) then -- if there are some tracks - at least one -- get their ids
    copy (get database ID of every track of currentList) to currentIDs -- list
   end if
  on error errText number errnum
   if errText does not contain "Invalid index." then
    error errstr number errnum
   end if
  end try
  
  --loop thru the selected tracks
  set sel to selection
  repeat with aTrack in sel
   set thisTrack to (get location of aTrack)
   set dbid to (get database ID of aTrack)
   try
    --add the track to playlist or show error
    if currentIDs does not contain dbid then -- if id not already present add the track
     add thisTrack to currentList
     set newCount to newCount + 1
    else
     set existsCount to existsCount + 1
    end if
   on error
    set deadCount to deadCount + 1
   end try
  end repeat
  
  
  set myMessage to "Added " & newCount & " track(s) to " & myList & "."
  if existsCount > 0 then
   set myMessage to myMessage & " There were " & existsCount & " selected track(s) already on it. "
  end if
  if deadCount > 0 then
   set myMessage to myMessage & " There were " & deadCount & " selected track(s) that were not added due to error. "
  end if
  set myReturn to my growlMessage(myMessage)
  
  set fixed indexing to oldfi
 end tell
else
 --itunes not running, quit
 set myMessage to "iTunes is not running"
 set myReturn to my growlMessage(myMessage)
 return
end if

--subroutine showing messages in growl (preferably)
--and if no growl, default dialog with timeout
to growlMessage(myMessage)
 --show our output message
 -- Check if Growl is running:
 set isRunning to my growl_is_running()
 
 --Only display growl notifications if Growl is running:
 if isRunning = true then
  
  tell application "GrowlHelperApp"
   -- Make a list of all notification types:
   set the allNotificationsList to ¬
    {"Notification 1", "Notification 2"}
   
   -- Make a list of the default enabled notifications:
   set the enabledNotificationsList to ¬
    {"Notification 1"}
   
   -- Register the script with Growl
   -- using either "icon of application"
   -- or "icon of file":
   register as application ¬
    "add_selected_to_playlist" all notifications allNotificationsList ¬
    default notifications enabledNotificationsList ¬
    icon of application "Script Editor"
   
   -- Send a notification:
   notify with name "Notification 1" title "Track add output" description myMessage application name "add_selected_to_playlist"
  end tell
 else
  tell currentApp
   activate
   display dialog myMessage giving up after 1
  end tell
 end if
end growlMessage

--sub checks if growl is running
on growl_is_running()
 tell application "System Events" to return (exists process "GrowlHelperApp")
end growl_is_running

--checks if itunes is running
on itunes_is_running()
 tell application "System Events" to return (exists process "iTunes")
end itunes_is_running
Name the file "add_selected_to_playlist.scpt" and save as a "Script" in /Users/YOU/Library/iTunes/Scripts/ and it will be available from the iTunes "Script" (script icon in the menu bar) pull-down menu.

Friday, August 7, 2009

Applescript to add 1 to play count of selected tracks

This script solves an issue related to recorded radio programs and iTunes Smart Playlists. Let's say for example you ripped an episode of "Crap from the Past", and you've set up these episodes to be automatically be added to a Smart Playlist called "programs". This Smart Playlist has a rule to exclude programs with a play count of "1" or greater (i.e. programs you've already listened to), to keep the list clean.

BUT...you don't like this particular episode. Maybe Ron "Boogiemonster" Gerber chose a theme like "Charity Songs from the 80s" or something...and you don't want it appearing in your Smart Playlist. Podcasts have a handy right-click option of "Mark as Not New" which solves the issue, but regular tracks do not. This script forces the play count to 1 so that it can be excluded from your Smart Playlist. Niche issue, I know, but a real concern for me!

Doug at Doug's Appplescripts for iTunes has already addressed this issue with the very nice "Add or Subtract Play Count", which is great...but it prompts you for a number. Since I'm all into efficiency and I only ever need to increase the play count by 1, I've hacked the script so that there is no prompt. I've also added error/success output with Growl because it's less obtrusive.
set myMessage to ""
tell application "iTunes"
 
 -- if no tracks selected, exit with message
 if selection is {} then
  set myMessage to "No tracks selected"
  set myReturn to my growlMessage(myMessage)
 else
  --loop thru selected tracks and add 1 to their play count
  set sel to selection
  repeat with aTrack in sel
   -- skip tracks without played count property
   if aTrack's class is file track or aTrack's class is URL track then
    tell aTrack
     set curPlayCount to (get played count)
     set played count to curPlayCount + 1
    end tell
   end if
  end repeat
  
  --success message
  set myMessage to "Added 1 to play count of selected tracks"
  set myReturn to my growlMessage(myMessage)
 end if
 
end tell

--subroutine showing messages in growl (preferably)
to growlMessage(myMessage)
 --show our output message
 -- Check if Growl is running:
 set isRunning to my growl_is_running()
 
 --Only display growl notifications if Growl is running:
 if isRunning = true then
  
  tell application "GrowlHelperApp"
   -- Make a list of all notification types:
   set the allNotificationsList to ¬
    {"Notification 1", "Notification 2"}
   
   -- Make a list of the default enabled notifications:
   set the enabledNotificationsList to ¬
    {"Notification 1"}
   
   -- Register the script with Growl
   -- using either "icon of application"
   -- or "icon of file":
   register as application ¬
    "add_play_count" all notifications allNotificationsList ¬
    default notifications enabledNotificationsList ¬
    icon of application "Script Editor"
   
   -- Send a notification:
   notify with name "Notification 1" title "Add Play Count Message" description myMessage application name "add_play_count"
  end tell
 else
  tell currentApp
   activate
   display dialog myMessage giving up after 1
  end tell
 end if
end growlMessage

--sub checks if growl is running
on growl_is_running()
 tell application "System Events" to return (exists process "GrowlHelperApp")
end growl_is_running
Name the file "add_play_count.scpt" and save as a "Script" in /Users/YOU/Library/iTunes/Scripts/ and it will be available from the iTunes "Script" (script icon in the menu bar) pull-down menu. I use this often, so I set up a Quicksilver keyboard hotkey trigger for it. Enjoy!

Saturday, July 25, 2009

Apple and Amazon Demonstrate Device Douche-baggery

Apple lays the smackdown on some of their own customers

The recent upgrade for iTunes contains a "feature" which basically locks out the Palm Pre. Before, the Pre would show up as an iPod in iTunes, allowing Pre users to sync it with their music an podcast libraries. This was a big selling point for the Pre.

I am not a Pre user, but I think this is a real dick move on Apple's part. People who use the Pre with iTunes, are at some level, customers of Apple. They are likely mac users...and even if they are not, they are at least using iTunes and therefore likely using it to purchase music. Basically, they are pissing off their own customers. Never a good strategy.

I'd be willing to bet that many people who purchased the Pre did so because they are stuck in egregiously long contracts with Sprint. Otherwise, they'd be iPhone users. If I was in this boat, and Apple pulled these kind of shenanigans, I'd be totally turned off to the idea of ever switching to the iPhone when my Sprint contract expired. Who wants to support a company who treats their customers like this?

That Apple would take such a drastic, dick measure indicates to me that they think the Palm Pre is a real threat to their smartphone business. They are scared of it, and have reacted out of fear. If I was them I'd a have stuck with their general, conceited, "you can't be serious" attitude towards competition. They should feel confident that their product is superior to any other smartphone, which it is. By taking such drastic action against the Pre, they have legitimized the Pre as a competitor in the mind of the marketplace.

Competition is good. It breeds innovation and keeps prices in check. The way to gain the competitive edge is not to take rash, semi-monopolistic measures against competitors, but to beat them fair and square with a quality product. I think Apple needs to hear it from customers, whether they use the Pre or not, that we don't appreciate this Microsoft-like behavior.

Apparently, Palm released an update to their software which once again allows it to sync with iTunes. It remains to be seen how Apple will react to this.

Link: http://gadgetwise.blogs.nytimes.com/2009/07/24/palm-pre-update-syncs-with-itunes-again/

Amazon, like a thief in the night

In a somewhat ironic move, Amazon took the unprecedented step of remotely deleting some of George Orwell's books from all Amazon Kindle devices of customers who had paid for them. Apparently, they were added to the Amazon store by a source who did not have the rights to them.

Sure, Amazon did refund the customer's money, but there are two very disturbing elements to this story. First, the privacy issue. Most customers were unaware that Amazon had the right and the means to remotely delete content from the Kindle. This is an invasion of privacy and it raises questions as to what else Amazon is doing remotely. Recording customer behavior? Monitoring other content users put on their Kindle?

Amazon swears up and down they will never do this again. But I think there needs to be independent action (seems like a great case for the EFF). There should be updates to the contract between manufacturer and the user, and updates to the software that will prevent any access to the device that the user does not explicitly authorize.

The other issue is, why is Amazon not vetting the content they are selling in their store? The issue here is between this "unauthorized" source and Amazon. The customers are unwitting participants in this dilemma, and end up getting the raw end of the deal. Amazon should pursue and work out the issue with the source.

Link: http://www.nytimes.com/2009/07/18/technology/companies/18amazon.html

Tuesday, July 14, 2009

"Smarter" iTunes Smart Playlists for podcasts

iTunes Smart Playlists are a great way to auto-populate playlists that you listen to often. I listen to alot of podcasts, so I created a "programs" playlist that is populated by podcasts I subscribe to, as they become available. Of course, Smart Playlists are not just for podcasts, but I'll use podcasts for illustration's sake in this how-to.

As neat as they are, Smart Playlists do have one major limitation, there is no way to mix boolean "AND" and "OR" expressions. To illustrate, let's say I wanted to create a Smart Playlist containing the "Marketplace" and/or "Planet Money" podcast, but only ones added in the last two days (new episodes). Simple concept, but it's actually not possible with a single Smart Playlist. See below:

Why a single Smart Playlist won't work
If I chose the match "all" following rules option, I would get a list with no podcasts on it (because there are no podcasts that are labeled both "Marketplace" and "Planet Money"). If I choose the "any" matching method, I would get the podcasts I desire, but I'd also get every other podcast that's arrived in the last two days (because of the third rule). Not quite what I'm looking for.

The good news is that there is a solution. One of the rules you can select basically says "...and is on the playlist x". That's the key! So, we can create a playlist-of-playlists, or nest "member" playlists, containing rules specific to each podcast, to get what we want on a single playlist. This helps us achieve the mixing of boolean AND and OR expressions that a straight Smart Playlist can't do.

Step 1: Create a new playlist folder in iTunes. We'll call it "nested" for purposes of illustration. Creating a folder is not absolutely necessary, but since these playlists aren't going to be directly accessed, I like to keep them contained for housekeeping reasons (you can collapse it to save space).

Add New Playlist Folder

Step 2: Create individual member Smart Playlists within the "nested" folder. Each playlist should contain the specific rules you want for each individual podcast you want on your master playlist. For example, one playlist for new "Marketplace" episodes within the last two days, and another for new "Planet Money" episodes within the last two days. (Note: There are a variety of ways to match a podcast - by Album is what I use because it's consistent). Be sure to match by "all following rules" on your member playlist. Also, if you're making a playlist of podcasts, be sure to add a rule, "Play Count is 0" to exclude podcasts you've already listened to. Don't do this if your playlist is of music.

Smart Playlist Rules

Step 3: Create a "master" Smart Playlist that will contain our nested member playlists. First, click away from the "nested" folder (click on Library->Music) so we can create a top-level iTunes Smart Playlist (we don't want this one inside the "nested" folder). In the Smart Playlist dialog box, make sure to select "any" as the matching method (very important), and add one rule for each member playlist you created. In this example the first rule is "Playlist contains Marketplace" and second is "Playlist is NPR: Planet Money". When you click "OK" you can name the playlist whatever you wish. I'll call it "Awesome Business Podcasts".

Creating the "Master" Smart Playlist

And voila! There's our incredible new complex-rules Smart Playlist. New "Marketplace" or "Planet Money" episodes will automatically arrive and old or previously-listened shows will automatically drop off.

The finished "master" Smart Playlist

Gettin' Tricky

There's about a bazillion different things you can do with Smart Playlists, and even more when you create complex Smart Playlists like we did above.

But one problem I ran into even with these "smarter" Smart Playlists is how to handle when, for example, I want to make a "finance" playlist all new Planet Money and Marketplace episodes (like the example above), but also a handful of select finance programs (podcasted or not) from the past.

Easy enough! Just create a nested member playlist called "Manually-Managed" (note: this is a regular playlist, not a smart playlist) and added my desired past episodes to that. I then include that "Manually-Managed" playlist on my master playlist. Here's a snot of my master podcast playlist, along with the nested member playlists that make it up:

My Smart Playlist Setup for Podcasts

You can add unlimited nested "member" playlists to a "master" playlist. I've heard some grumbling that it sucks up computer resources to do this, but that is not my experience.

One neat thing to note about Smart Playlists is that you can manually re-order them. So if I wanted to listen to Marketplace first, I just drag it to the top. If you are going to use this playlist on an iPod or iPhone, be sure to set it to sync whenever you plug it back into the computer. That way it will update your library with the play count (and therefore update the Smart Playlist).

Here are some links detailing other great Smart Playlists you can try:

Thursday, July 9, 2009

Top 10 "This American Life" episodes

The greatest radio show (and podcast) ever. Possibly the best show ever, in any media. I look forward to every episode, and get seriously bummed out when it's a re-run. In case you're interested, here's my top 10 favorite episodes. I've listened to each of these multiple times, and will continue to!
  1. #355 The Giant Pool of Money - Definitely the best explanation of what went wrong with the US (and world) economy, and is referred to as such by other media and even politicians. I believe this is their most popular show to date (even topping "Squirrel Cop"). This episode was so well-received it spawned a delightful new show, NPR's "Planet Money" which podcasts thrice weekly. There were a few great sequels to this episode. If you group them together you have just about the best layman's-terms synopsis of the financial meltdown put together so far. They are: #365 Another Frightening Show About the Economy, #375 Bad Bank, #377 Scenes from a Recession and #382 The Watchmen.
  2. #304 Heretics - Great story about how this minister, Carlton Pearson, decided one day that hell was a human creation, a method of controlling people. And he wasn't going to believe in it anymore.
  3. #168 The Fix Is In - Classic episode about how a clown of a middle manager exposed the biggest price-fixing scheme in history. Looks like this is coming out in movie form, as a comedy: The Informant!
  4. #261 The Sanctity of Marriage - Helped to put me off the idea of marriage altogether (ok...I'm just chicken). The opening bit with John Gottman is a must-hear. For more from the guy, there's also this great podcast, and this one.
  5. #179 Cicero - The story of Cicero, IL...probably the most corrupt city in America!
  6. #254 Teenage Embed, Part Two - A young Afghan-American goes to Afghanistan with a tape recorder, and records events as they unfold. His father has returned to Afghanistan to serve in the newly-formed government. It's a follow-up to the original episode #230 Come Back To Afghanistan, which was also very good.
  7. #290 Godless America - This is on here strictly for the Julia Sweeney segment. She tells the story of how she fell away from Catholicism. Hilarious.
  8. #127 Pimp Anthropology - If you ever wanted to know how pimps work...
  9. #220 Testosterone - Dedicated to the chemical which is the essence of male-ness.
  10. #84 Harold - Being from Chicago, I love the Chicago stories. This whole show was dedicated to the story of Harold Washington, the first black mayor of a major American city. Fascinating guy.

Monday, June 29, 2009

Use perl and applescript to keep playlists synced with iPhone/iPod

I am always adding to and editing iTunes playlists, particularly the more oft-listened ones like my "punk", "ambient", and "programs & podcasts" playlists. Since I listen to alot of stuff on my iPhone, I naturally want these playlists always available and up-to-date on my device. I can't tell you how many times I plugged in my iPhone to my car stereo and found, to my disappointment, that I forgot to drag-and-drop the most recent version of a playlist to the device.

Why not just set the iPhone to automatically update? Because I want the finite control that manually managing affords me. (UPDATE: to all you haters out there wondering why I don't just use auto-sync...because it "locks" the iPod so you can't manually manage any playlists. I want to manually manage most things and just have selected playlists update automatically. If you don't like this script don't use it!)

So I created a script that updates a specified list of playlists to all attached iPods/iPhones. It can run on a schedule (with cron), so I never have to worry that my iPhone won't have the most recent versions of my playlists!

Features
  • Adds specified playlists to all iPods attached to your mac
  • Will skip the playlist if there's not enough space on an attached device
  • Retains the order in which the tracks appear on the library playlist
  • Outputs useful debug info
Usage

First, the script requires the iPod/iPhone be set to "Manually manage music and videos". I named the script "updateipod.pl" and placed in my user's "bin" directory. It's recommended you specify the playlists that you want to sync in the @playlists array at the top of the script:
my @playlists = (

 "jamz",
 "0pods",
 "punk best",
 "stoner best"
 
 );
After chmod-ing the script to 755 just invoke it as you would any script:
/Users/user/bin/updateipod.pl
Optionally, you can specify the names of the playlists as arguments on the command line. Useful for one-off updating. Note that if playlists are passed as command-line arguments they will be processed instead of whatever is specified in the @playlists array at the top of the script. Make sure to enclose playlist names in quotes if the playlists have spaces or special characters. For example, here's how I'd send three playlists to my connected devices on the command line:
/User/user/bin/updateipod.pl "michael best" 0pods "80's by rory"
I schedule it to run hourly with cron:
0 * * * * /Users/user/bin/updateipod.pl > /dev/null 2>&1
But you can do this if you want more output:
0 * * * * /Users/user/bin/updateipod.pl >> /Users/user/Desktop/ipod_update_log.txt 2>&1
That's it! Here's the script. Click "expand source" below to view:
#!/usr/bin/perl

# 2009-07-05
# Auto-sync specified playlists to all attached iPods/iPhones
# Specify playlist names in the @playlist array OR enter them
# as arguments (in single quotes) on the command line like this:
#
# $ ./updateipod.pl "michael best" "0pods" "80's by rory"
#
# I run this hourly with the cronjob:
#
# 0 * * * * /Users/user/bin/updateipod.pl > /dev/null 2>&1
# 
# use something like this if you want to see the output for each run:
# 
# 0 * * * * /Users/user/bin/updateipod.pl >> /Users/user/Desktop/ipod_update_log.txt 2>&1
#
# some applescript in this script was borrowed from "Selected Playlists To iPod v2.1" at the very excellent http://www.dougscripts.com/

use strict;
use POSIX qw(strftime);

my @playlists = (

 "jamz",
 "0pods",
 "punk best",
 "stoner best"
 
 );
 
# if there are playlists entered as arguments on the cmd line
# those are gonna trump whatever is specified above
if ($ARGV[0]) {
 @playlists = @ARGV;
}

my $time = strftime( "%Y-%m-%d %H:%M:%S",localtime(time));
print "syncing of playlists started at $time\n";

# check if itunes is running, start if not
checkIfRunning();

# get the sources of any attached ipods.  exit if there are none attached.
my ($id_ref,$name_ref) = getiPodSources();
my @ipod_ids = @$id_ref;
my @ipod_names = @$name_ref;
my $devices = @ipod_ids;

# exit if we have no attached ipods
if (!$ipod_ids[0]) {
 print "no manually managed iPods detected, exiting...\n";
 exit;
}

# make sure the playlists exist in the itunes lib.
my @confirmed_playlists = ();
foreach my $playlist (@playlists) {
 my $exists = checkPlaylistExists($playlist);
 if($exists) {
  push(@confirmed_playlists,$playlist);
 } else {
  print "\"$playlist\" is not a valid playlist in the iTunes library, skipping...\n";
 }
}

# make sure we have at least one valid playlist
if (!$confirmed_playlists[0]) {
 print "no valid playlists entered, exiting...\n";
 exit;
}

# loop thru our ipods first, then within that,
# loop thru our playlists array and sync them to each ipod
for (my $i=0;$i<$devices;$i++) {
 foreach my $playlist (@confirmed_playlists) {
  print "telling itunes to sync playlist \"$playlist\" to $ipod_names[$i]\n";
  my $output = syncPlaylist($ipod_ids[$i],$ipod_names[$i],$playlist);
  if ($output ne '1') {
   print "${output}, skipping...\n";
   next;
  }
 } # end loop thru playlists
} # end for thru ipods

print "done.\n\n";
exit;

#################################################################################################
###################                      SUBROUTINES                   #####################
#################################################################################################

sub stripNonAlph {
 # strips non-alphanumeric characters from strings
 
 my($text) = shift;
 $text =~ s/([^0-9a-zA-z ])//g;
 return ($text);

} # end sub strip non alph

sub checkIfRunning {
 # checks if iTunes is running
 # starts it if not
 
 my $start=`/usr/bin/osascript <<END
 --start the program if necessary
 set isRunning to 0
 tell application "System Events"
  set isRunning to ((application processes whose (name is equal to "iTunes")) count)
 end tell
 if isRunning is 0 then
  tell application "iTunes"
   activate
  end tell
 end if
 `;
 
} # end sub check if running

sub getiPodSources {
 # gets the system source IDs of any attached ipods
 # (assumes there's more than 1 in case there actually are)
 # returns in semicolon-delimited list
 
 my $source=`/usr/bin/osascript <<END
 tell application "iTunes"
  set validiPods to {}
  set myPods to ""
  set myPodsNames to ""
  set myPodReturn to ""
  set ipodSources to (get every source whose kind is iPod)
  if ipodSources is {} then
   return 0
  end if
  
  repeat with s in ipodSources
   try -- can't make a playlist on an iPod set to sync automatically, so this will fail.  
    set tp to make new user playlist at s with properties {name:("p)" & my get_temp_name() & "-^")}
    delete tp
    set end of validiPods to s
   end try
  end repeat
  
  if validiPods is {} then
   return 0
  end if
  
  repeat with i from 1 to (length of validiPods)
   set myPods to myPods & ";" & id of item i of ipodSources
   set myPodsNames to myPodsNames & ";" & name of item i of ipodSources
  end repeat
  
  set myPodsReturn to myPods & "|" & myPodsNames
  return myPodsReturn
  
 end tell
 to get_temp_name()
  return (get time of (get current date)) as text
 end get_temp_name
 END`;
 
 chomp($source);
 $source =~ /^;(.*?)\|;(.*?)$/;
 my ($id,$name) = ($1,$2);
 my @ids = split(/;/,$id);
 my @names = split(/;/,$name);
 return (\@ids,\@names);
 
} # end sub getiPodSources

sub checkPlaylistExists {
 # checks if the itunes playlist exists
 
 my $playlist = shift;
 my $exists=`/usr/bin/osascript <<END
 tell application "iTunes"
  if (exists playlist "$playlist") then
   return 1
  else
   return 0
  end if
 end tell
 END`;
 $exists = &stripNonAlph($exists);
 return ($exists);
 
} # end check if playlist exist

sub syncPlaylist {
 # takes the id of the source (ipod) and the name of the playlist to sync as arguments
 
 my ($ipod_id,$ipod_name,$playlist) = @_;
 my $sync = `/usr/bin/osascript <<END

 tell application "iTunes"
  set playlistName to "$playlist"
  set main_lib to library playlist 1
  set ipod_src to item 1 of (get every source whose id is $ipod_id)
  set ipod_lib to library playlist 1 of ipod_src
  set playlistSize to 0
  set iPodFreeSpace to free space of ipod_src
  set copySuccess to 0
  set playlistSuccess to 0
  
  set myPlaylists to (get every user playlist)
  repeat with thisPlaylist in myPlaylists
   if name of thisPlaylist is playlistName then
    
    --loop thru the playlist to determine if there is enough
    --free space on the ipod for any songs not already on it
    set eop to index of last track of thisPlaylist
    repeat with i from 1 to eop
     --determine if the song is already on ipod lib
     set i_track to track i of thisPlaylist
     if (get class of i_track) is file track then
      tell i_track to set {nom, art, alb, siz, kin, tim, pid} to {get name, get artist, get album, get size, get kind, get time, get persistent ID}
      try
       -- is track already in iPod library?  setting this will fail if not
       set new_i_track to (some track of ipod_lib whose name is nom and artist is art and album is alb and size is siz and kind is kin and time is tim)
      on error -- if not, add size of track to running total of tracks not on ipod
       set playlistSize to playlistSize + siz
      end try
     end if
    end repeat --thru selected playlist
    
    if free space of ipod_src is greater than playlistSize then
     -- establish iPod playlist
     try
      --does the iPod playlist already exist?  if so we delete it.
      --why?  because if we add additional tracks to the end of it, the track list will no be in the 
      --same order as they are on our iTunes playlist
      set new_ipodplaylist to user playlist playlistName of ipod_src
      delete new_ipodplaylist
      set new_ipodplaylist to (make new user playlist at ipod_src with properties {name:playlistName})
     on error
      -- if not, create the new iPod playlist
      set new_ipodplaylist to (make new user playlist at ipod_src with properties {name:playlistName})
     end try
     
     --I'm not checking to see if the tracks already exist on the ipod before copying
     --because it *appears* itunes won't duplicate them.  maybe this is a new feature
     try
      duplicate (every track of thisPlaylist) to ipod_lib
      set copySuccess to 1
     on error
      return "error copying tracks to iPod $ipod_name"
      set copySuccess to 0
     end try
     try
      duplicate (every track of thisPlaylist) to new_ipodplaylist
      set playlistSuccess to 1
     on error
      return "error duplicating track listing to playlist $playlist on iPod $ipod_name"
      set playlistSuccess to 0
     end try
     
     if copySuccess is 1 and playlistSuccess is 1 
      return 1
     end if
     
    else
     return "not enough free space on iPod $ipod_name to copy all tracks on $playlist"
    end if
    
   end if --current playlist = selected playlist
  end repeat
  
 end tell
 `;
 $sync = &stripNonAlph($sync);
 return ($sync);
 
} # end sub syncPlaylist

__END__
Some of the applescript was borrowed from this script at the very awesome Doug's Applescripts for iTunes.

Record iPhone conversations on a Mac with WireTap

I know what you're thinking: this seems creepy. However, there is actually a legitimate purpose here. The reason I want to do this is to record work-related conference calls to supplement my notes. Oftentimes, even with notes, it's unclear as to exactly what was discussed and decided. Disclaimer: there are state laws regarding recording of phone calls you should be aware of if you're going to do this. Google it.

The way I do this is to split the output (the sound coming from the other end of the line) from the iPhone - one signal to the earbuds so you can hear it and one to the computer's line-in input for recording. The input (your voice) is heard by the folks at the other end because there's a mic on your earbuds picking it up and sending it back to the iPhone. However, the computer can't pick that up, so I configure the recording device (in this case, WireTap) to also record what's coming in the computer's built-in mic. This way you've got both sides of the conversation available in the recording. This is one of the great things about WireTap - it can record from two sources at once.

Getting Started
There are a few things you'll need to get if you don't already have:
  • An iPhone (actually any phone that has a mini-jack in/out will work)
  • Earbuds with microphone. The ones that came with the iPhone work well. I have these and they are great. I'm sure a headset would work too.
  • A mini-jack splitter (couple bucks at RadioShack)
  • Male-to-male mini-jack cable (couple bucks at RadioShack)
  • WireTap Studio software. Despite the name, recording phone coversations is actually not what it's designed for! It's a bit spendy at $69, but you can do a free 30-day trial with no software limitations.
What You Need
Setting Up
Plug the splitter into the jack of the phone. Then plug your earbuds/headset into one of the splitter's jacks, and the direct male-to-male cable into the other. Plug the other end of the male-to-male cable into the microphone input on your mac.

Setup
Starting the Recording
Once you have the phone and cables in place, start up WireTap. if the small black controller isn't visible, go Window (menu) -> Controller to view it. In the controller window, configure the first input to be "Line In", and the second input to be "Internal Microphone". See below for example.

WireTap Controller

At this point, you may want to do a test call to be sure your levels are correct. Just hit the round "record" button on WireTap's controller and place a call (maybe to a friend or an 800 number you know will be answered by an automated system, like your bank). Be sure that you hear at least 30 seconds of sound from the other end, and make sure you talk as well to get an accurate sampling of sound from both ends.

After hanging up, hit the "stop" button on WireTap's controller, and switch to the "Library" window. (Window Menu -> Library). Your recording will probably be called something like "Built-in Input_recording". You can highlight it and play it right from that window, or you can double-click it to view in the editor. When viewing in the editor, the neat thing is that WireTap shows the sound waves for each source separately (they are color-coded). In the example below, my voice is purple while the caller's voice is grey.

Phone Sound Waves
Listen to the levels, if one side is too low or too high, you can adjust the levels. Go System Preferences -> Sound to open the Sound Settings window. The two lines you want to edit are the "Internal Microphone" and "Line In". You can even do this during your test recording so that you are seeing the actual sound in the level meters. If too low or too high, adjust slider bars. I've found checking the box "Use ambient noise reduction" for the Internal Microphone works well.

System Sound Settings
Once you have your levels right and can hear both sides of a test conversation, you're ready to record! You can export your recordings from the WireTap library as MP3s (highlight your recording and hit the "Local" button), which is good if you want to catalog elsewhere on your computer.

Update 2009-07-23: by request, here's an example of a recorded phone conversation using this method:

Use applescript and keyboard shortcuts to skip forward or back in iTunes

When I'm listening to long audio files, like podcasts or time-shifted programs, often I want to skip ahead an exact duration of time. For example, if the program has commercials, I want to skip ahead exactly 3 minutes. If I overshoot, then I want to skip back 10 seconds until I hit the program's return from break. I set up a set of applescripts that I trigger with Quicksilver keyboard shortcuts (see short how-to on adding hotkeys with quicksilver). This prevents me from having to actually go to iTunes, and manually move the time slider forward and back until I get the right position.

Here's the script for moving forward 3 minutes:
tell application "iTunes"
 if player state is playing then
  -- length of current track:
  set trackTime to duration of current track
  --get the current position time:
  set currTime to get player position
  set currSkip to currTime + 180
  
  -- checks if new position is greater than 
  -- length of track, and corrects it if it is:
  if currSkip > trackTime then
   set currSkip to trackTime
  end if
  
  set player position to currSkip --skip to new position
 end if
end tell
...and here's one for skipping back 10 seconds:
tell application "iTunes"
 if player state is playing then
  --get the current track time:
  set currTime to get player position
  if currTime < 10 then
   --go to start of the track:
   set currSkip to 0
  else
   --otherwise, skip backwards 
   set currSkip to currTime - 10
  end if
  
  set player position to currSkip --skip to new position
 end if
 
end tell
Note that the time is kept in seconds (not minutes). So to create scripts for varying amounts of time just edit the seconds and save as a separate script. Save in your user's "scripts" folder, ~/Library/Scripts/ as something like "skip3min.scpt".

Thursday, June 25, 2009

Top 10 internet radio programs

So, the difference between these and my top 10 podcasts is that these either a) are not available as podcasts, usually for music copyright reasons or 2) are available as podcasts but only if you pay a subscription fee. As such, like any good radio program, these are prime candidates to be time-shifted so you can listen when you want.

Here are my faves:
  1. Overnight Groove - Saturdays at 3-5AM PT...this happens to be when I am sleeping, so...time-shift! 70s and 80s dance music of the finest quality. Emphasis on the less hit-radio tracks, though those are peppered in for good measure. Awesome program if you like to feel great.
  2. Crap from the Past - The name says it all. DJ Ron "Boogiemonster" Gerber calls it "like a graduate-level course in pop music". The guy is an insane encyclopedia of pop music knowledge, and a real DJ in the classic sense. His brilliant intro spot says it all. And...he loves Yacht Rock.
  3. Quiet Space - For ambient music lovers, this is the best-curated program available on the internet. DJ Paul Gough (aka the artist pimmon) plays an hour's-worth of the best ambient he can find, both old and new, twice a week.
  4. Moneytalk - Bob Brinker's a ruthless old codger bent on belittling politicians and even listeners he thinks are stupid (which is alot of them). He has a 3-hour call-in show on Saturdays and Sundays (I use KGO's stream which is very reliable). Best part of the show is the special guest, who is featured in the last hour of each show. Only downside is that the commercials are frequent and annoying. But, as long as the program is time-shifted, you can just skip over them.
  5. Sonic Reducer - The best punk/hardcore/garage rock show on the internets. 3 hours of hard rock bliss every Saturday night. Gets its name from a song by early Cleveland punk band The Dead Boys, a favorite of Anthony Bourdain. How's that for a tangential anecdote.
  6. Philosophy Talk - "The show that questions everything...except your intelligence." I love this program because these guys, professors of Philosophy at Stanford, have great radio personalities and make complex subjects understandable. Not every show is a hit, but most are.
  7. Sleepbot Environmental Broadcast - This isn't a radio program per se, as it's always on and streaming, 24 hours a day. But I time-shift it because while most of what the curator (Dan Foley) plays is great, sometimes his tastes and my own diverge...and I like to skip over those tracks. I record about 3 hours of this nightly so I can listen during the day while I work. I've found alot of great music on this station.
  8. The Next Big Thing - This show no longer airs, unfortunately. It was on select public radio stations from 1999 to 2005. It's on this list because there is a streaming archive (but no podcast, strangely) that features the vast majority of the shows. The show was a fine example of radio documentary, produced by WNYC. I don't know why it ended, but the WNYC "radio doc" slot seems to have been filled by RadioLab, another fine program. Anyway, see my post about recording "The Next Big Thing" archived shows.
  9. Surface Noise - DJ Lounge Laura on WMNF in Tampa - and myself - have compatibly eclectic tastes in rock. Her starting point is (roughly) punk in the 80s, but she takes wide detours from that format...glam, classic rock, pop, whatever. Like Ron "Boogiemonster" Gerber, she's a DJ in the classic sense of the term. Only downside is that the sound quality of the stream is pretty low, but not so much so that it prevents me from listening.
  10. The Dave Ramsey Show - I am a Dave Ramsey devotee. I don't agree with all his views, but in terms of opinions on personal finance (it's actually more about personal responsibility), he's dead on. He's not afraid to lay the smackdown on his listeners, but they tend to deserve it. A little tough love is good every now and then.

Top 10 podcasts

I listen to ALOT of podcasts. Not that anyone should care, but these are my top 10 podcasts. Part of my continuing series of "top tens". Obviously very heavily weighted towards public radio programs...but I am not a socialist.
  1. This American Life - Still the best hour of info-tainment(?) docu-tainment(?) available!
  2. Marketplace - I listen to this every day.
  3. NPR's Planet Money - Sort of like a "This American Life" but only concerned with money/finance and it's 3x a week. What I love about it is it translates all the financial gibberish that's in the news into something I can understand. It's also relatively politically un-biased (at least in terms of finance) for NPR.
  4. Stuff You Should Know - Josh Clark and Chuck Bryant are a couple of the funniest info-tainers(?) on the internets! I learn about random things like how to survive plane crashes, how lobotomies work, and so forth . Twice a week. Nice, Chuck.
  5. EconTalk - Since the financial meltdown everyone's become an amateur economist, myself included. This show gives me just enough of an education to be dangerous. What I like about EconTalk is that the host, Russ Roberts, is very fiscally conservative but isn't afraid to have guests on who have vastly different opinions than him. Makes for great conversation.
  6. American RadioWorks - 60 minutes, one subject explored in-depth. Basically, it's like "Frontline" for radio. VERY high-quality documentaries. It would be closer to the top on my list, but they are not that frequent. I think it's because they are so well-made they take a while to produce.
  7. RadioLab - Hard to really describe this show. It's documentary, science, drama and artful sound design rolled into one. It's beautiful and captivating. Here, I'll let one of the show's hosts, Jad Abumrad, explain:
  8. The Moth - Bringing back the art of storytelling! The podcast is sort of a "best of" stories, chosen from years worth of archives.
  9. Cosmic Slop - A collection of not-oft-heard tracks, mostly from the 70s and 80s. Chuck and Joel are great radio personalities, and have encyclopaedic knowledge of popular (and in this case, not-so-popular) music. These guys have intorduced me to alot of real gems. I owe my love of Joan Armatrading to them. Great stuff, I just wish it was more frequent.
  10. BBC Radio Documentaries - Selected radio docs that run on BBC throughout the week. Usually 20-25 mins long. Can be hit-or-miss, but I have heard some really great ones.

Record WNYC's "The Next Big Thing" streams to MP3

Remember that show "The Next Big Thing" in WNYC? Good show. Ran from 1999 to early 2006. I miss it. Fortunately, they have most of the shows archived on the WNYC site. Unfortunately, they are in realaudio. When I tried to listn to the realaudio stream on my mac with the crappy realaudio player (are they even still in business?? If so, why?), I got a random collection of clicks and blips.

But I soon found out that mplayer will play them. So I wrote up a little script that will rip the realaudio stream to MP3, which is better anyway because then I can listen on my iPod. The quality isn't great - I think it's a 32kbps stream, but that's roughly equivalent to AM radio quality. It's listenable. It will run on any unix-based system (like a mac) with the following pre-requisites:
  • mplayer
  • wget
  • lame
  • sox
  • Perl 5.8.8+
  • Perl Module: MP3::Tag
Note: you may need mplayer configured with realaudio codecs as per this article, but I tried it on a linux system that didn't have them, so it should work without.

One caveat is that although mplayer has the capability to dump realaudio streams very quickly (by adding a large value, like 5000000 to the -bandwidth switch), I found that for some reason alot of audio in these streams was skipped. Not good. So I left out the bandwidth switch and let it dump the stream in real time. Takes alot longer, but you get all the audio.

Click "expand source" to view the code below. I call it "nextbigthing.pl":
#!/usr/bin/perl

# 2009-06-21
# created to take a URL of a WNYC's "The Next Big Thing" show archives as input (first argument on cmd line)
# ex: $ ./nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/05
# and create tagged MP3s of each show on the page as output
# 
# PREREQs
# * mplayer with links to realaudio codecs (see here: http://www.macosxhints.com/article.php?story=20050130184054216)
# * wget
# * lame
# * sox

use strict;
use POSIX qw(strftime);
use MP3::Tag;

# config these
my $wget = "/usr/bin/wget";
my $mplayer = "/usr/bin/mplayer";
my $lame = "/usr/bin/lame";
my $sox = "/usr/bin/sox";
my $tmpfolder = "/home/user/tmp/tnbt/"; # create this if it doesn't exist
my $outfolder = "/home/user/Music/the_next_big_thing/"; # create this if it doesn't exist

my @rmfiles = ();

# get the url off the command line
if (!$ARGV[0]) {
 print "no URL found.  put the url in single quotes as the first argument after calling this program on the command line\n";
 exit;
} 

# wget the page
my $outfile = $tmpfolder . "tmpout.txt";
my $wout = `$wget -U 'Mozilla/5.0 (Windows; U; Windows NT 5.1) Gecko/20041001' --timeout=30 -t 2 -q --output-document=${outfile} $ARGV[0]`;
if (not(-e $outfile)) {
 myExit("nothing found. check url and try again",\@rmfiles);
} else {
 push(@rmfiles,$outfile);
}

# put file into array
open(FILE,"$outfile");
 my @lines = <FILE>;
close(FILE);

# search the array for keywords and place our show info into a hash
my %SHOWS = ();
my $counter=0;
my ($showdate,$episode_name,$episode_date,$episode_description);
foreach my $line (@lines) {
 chomp($line);
 
 # there's a really specific format we're looking for here.  if they change their template, this won't work at all
 # we're looking for show info here
 if ($line =~ /episodenamesmall/) {
  $lines[$counter] =~ /^.*?episodes\/(\d\d\d\d)\/(\d\d)\/(\d\d)">$/;
  $showdate = "${1}-${2}-${3}";
  $lines[$counter+1] =~ /^(.*?)</;
  $episode_name = $1;
  $lines[$counter+2] =~ /^<.*?>(.*?)<.*?>$/;
  $episode_date = $1;
  $lines[$counter+3] =~ /^<p>(.*?)<\/p>$/;
  $episode_description = $1;
  
  $SHOWS{$showdate}{'showdate'} = $showdate;
  $SHOWS{$showdate}{'episode_name'} = $episode_name;
  $SHOWS{$showdate}{'episode_date'} = $episode_date;
  $SHOWS{$showdate}{'episode_desc'} = $episode_description;
  
  
 } # end search for 'episodenamesmall'

 # we're looking for the url to the realmedia here
 if ($line =~ /<a class="listen"/) {
  my $rafiles;
  my @ramfiles = ();
  $lines[$counter+2] =~ /^\s+href="\/stream\/ram.py\?(.*?)" class.*?$/;
  $rafiles = $1;
  $rafiles =~ s/file[\d]{0,2}=//g;
  my @tmpfiles = split(/&/,$rafiles);
  foreach my $tmpfile (@tmpfiles) {
   my $fullurl = 'rtsp://raudio.wnyc.org/' . $tmpfile;
   push(@ramfiles,$fullurl);
  }
  my $print_ra = join("^",@ramfiles);
  @{ $SHOWS{$showdate}{'files'} } = @ramfiles;

 } # end search for 'listen'
 
 $counter++;
 
} # end foreach loop thru html file

# loop thru the hash, grab each show, and encode
for my $show ( sort keys %SHOWS ) {
 my @wavfiles = ();
 
 print "Episode $SHOWS{$show}{'episode_name'} - $SHOWS{$show}{'episode_date'}\n";
 
 # our filenames
 my $wavfilename = ${tmpfolder} . replaceSpace(stripChars($SHOWS{$show}{'episode_name'})) . '_' . $SHOWS{$show}{'showdate'} . '.wav';
 my $mp3 = $wavfilename;
 $mp3 =~ s/${tmpfolder}/${outfolder}/;
 $mp3 =~ s/\.wav/\.mp3/;
 # if an mp3 already exists in the dest. dir, skip
 if (-e $mp3) {
  print "\nFilename $mp3 already exists, skipping...\n\n";
  next;
 }
 
 foreach my $file (@{ $SHOWS{$show}{'files'} }) {
  #print "File: $file\n";
  $file =~ /^rtsp:\/\/raudio.wnyc.org\/nbt\/(.*?)$/;
  my $ra_name = $1;
  # dump the raw .ra file
  my $raw = "${tmpfolder}${ra_name}";
  # removed the bandwidth switch because although it speeds up the dumping, it seems to skip alot of audio.
  # so as of now it's just dumping in real-time
  #my $get_cmd = "$mplayer -bandwidth 5000000 -noframedrop -dumpfile $raw -dumpstream '$file'";
  my $get_cmd = "$mplayer -noframedrop -dumpfile $raw -dumpstream '$file'";
  print "MPLAYER DUMP COMMAND: $get_cmd\n";
  my $get_out = `$get_cmd`;
  # convert files to wav
  my $filename = "${tmpfolder}${ra_name}.wav";
  my $mplayer_cmd = "$mplayer $raw -vc dummy -vo null -af volume=0,channels=2 -ao pcm:waveheader:file=$filename";
  print "MPLAYER WAVE COMMAND: $mplayer_cmd\n";
  my $mp_out = `$mplayer_cmd`;
  if (-e $filename) {
   push(@rmfiles,$filename);
   push(@rmfiles,$raw);
   push(@wavfiles,$filename);
  } else {
   myExit("no wavfile was created from $mplayer_cmd",\@rmfiles);
  }

 } # end foreach thru wavfiles
 
 # cat wavfiles together with sox
 my $wavfilejoin = join(' ',@wavfiles);
 my $soxcmd = "(for wavfile in ${tmpfolder}*.wav; do $sox \"\$wavfile\" -t .raw -r 44100 -sw -c 2 -; done) | $sox -t .raw -r 44100 -sw -c 2 - -t .wav $wavfilename";
 print "SOX CONCAT COMMAND: $soxcmd\n";
 my $soxout = `$soxcmd`;

 # encode the wav file
 if (-e $wavfilename) {
  push(@rmfiles,$wavfilename);
  my $title = "The Next Big Thing: \"" . stripChars($SHOWS{$show}{'episode_name'}) . "\" (" . $SHOWS{$show}{'episode_date'} . ")";
  my $author = "Dean Olsher";
  my $composer = "WNYC";
  my $album = "The Next Big Thing";
  $SHOWS{$show}{'showdate'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
  my $year = $1;
  my $info_url = $ARGV[0];
  my $genre = "Talk";
  # caused some problems adding the ID3v2 stuff on cmd line so we'll just have MP3::Tag do it
  #my $id3 = " --add-id3v2 --ignore-tag-errors --tt \"$title\" --ta \"$author $composer\" --tl \"$album\" --ty \"$year\" --tc \"$info_url\" --tg \"$genre\"";
  #my $encode = "$lame -V0 -h -b 128 --quiet --vbr-new$id3 $wavfilename $mp3";
  my $encode = "$lame -V0 -h -b 128 --quiet --vbr-new $wavfilename $mp3";
  print "LAME ENCODE COMMAND: $encode\n";
  my $enc_out = `$encode`;
  
  # add proper ID3 tags and clean up
  if($mp3) {
   my $comments = stripChars($SHOWS{$show}{'episode_desc'});
   &id3v2tag($mp3,$title,$year,$author,$album,$info_url,$comments,$genre,"lame");
   myExit("SUCCESS! Encoding complete for $title",\@rmfiles,1);
  } else {
   myExit("the mp3 doesn't exist so I can't tag it",\@rmfiles);
  } # end if for mp3 exists
  
 } else {
  myExit("our resampled wav file doesn't exist so I can't encode an mp3",\@rmfiles);
 } # end if for wav exists
 
 print "\n\n";
 
} # end foreach thru shows

print "Done\n";
exit;

##################### SUBS ###########################

sub myExit {
 
 my ($message,$rmfiles_ref,$donotexit) = @_;
 my @rms = @$rmfiles_ref;
 foreach my $remove (@rms) {
  if (-e $remove) {
   print "REMOVING $remove\n";
   unlink($remove)
  }
 }
 print "$message\n";
 if (!$donotexit) {
  exit;
 }
 
} # end sub myExit

sub stripChars {

 my($text) = @_;
 
 $text =~ s/\n/ /g; # strip carraige returns
 $text =~ s/\t/ /g; # strip tabs
 $text =~ s/\a/ /g; # strip carraige returns
 $text =~ s/"/'/g; # strip quotes and replace with single quotes
 $text =~ s/\s+/ /g; # strip repeating spaces and replace with one
 return ($text);

} # end sub stripchars

sub replaceSpace {

 my($text) = shift;
 $text =~ s/([^\w+\s+])//g;
 $text =~ s/^\s+//;
 $text =~ s/\s+$//;
 $text =~ s/([\s+])/_/g;
 return ($text);

} # end sub replacespace

sub id3v2tag {
 # adds id3v2 tag to mp3s

 my ($file,$title,$year,$author,$album,$info_url,$comments,$genre,$encoder) = @_;
 my $mp3 = MP3::Tag->new($file);
 $mp3->get_tags();
 $mp3->new_tag("ID3v2");
 $mp3->{ID3v2}->add_frame("TALB", "$album");
 $mp3->{ID3v2}->add_frame("TIT2", "$title");
 $mp3->{ID3v2}->add_frame("TPE1", "$author");
 $mp3->{ID3v2}->add_frame("TCON", "$genre");
 $mp3->{ID3v2}->add_frame("TSSE", "$encoder");
 $mp3->{ID3v2}->add_frame("TYER", "$year");
 $mp3->{ID3v2}->add_frame("COMM", "ENG", "", "$comments $info_url");
 $mp3->{ID3v2}->add_frame("TIT3", "$comments $info_url");
 $mp3->{ID3v2}->add_frame("TRSN", "$comments $info_url");
 $mp3->{ID3v2}->add_frame("TXXX", "$comments $info_url");
 $mp3->{ID3v2}->add_frame("WORS", "$comments $info_url");
 $mp3->{ID3v2}->add_frame("WXXX", "$comments $info_url");
 $mp3->{ID3v2}->write_tag;
 $mp3->close();

} # end sub id3v2tag



The neat thing about this is it will retrieve the show info from the archive web page and add it to the ID3 info, so the MP3s are named and dated properly. It's called with the URL of each archive page like this:
./nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/05
I created a wrapper script that sends a whole bunch of the URLs to the script like this:
#/bin/bash

/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/06;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/07;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/08;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/09;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/10;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/11;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2002/12;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2003/01;
/home/user/bin/nextbigthing.pl http://www.wnyc.org/shows/tnbt/episodes/2003/02;
Then just let it run until all the streams are recorded!

Script for time-shifting internet radio programs (aka "Streamshifter")

There are alot of time-shifting internet radio stream recorders out there (i.e. TiVo for internet radio) like RadioShift and WireTap Studio, and I've tried them all. Inevitably I've run into limitations with them. Either they are unreliable (you go to listen to your program and find it didn't record and there's no clear reason why), they are memory hogs, or they can't be run from a server (they are gui programs mant to be run locally). So, I am in the process of writing my own program in perl. As of now it is very functional and feature-rich. As far as I can tell, it's the only stream recorder that can be run from a server (advantageous because the bandwidth is more reliable than a home connection) and administered from the web.

I encourage you to try it out on some unix-based system (I'm on a mac) and give me feedback. I'm always working to improve it. There's not much documentation at this time (just this post), but if you're a script hacker it's pretty easy to figure out. You'll need:
  • Perl 5.8.8 or higher (though earlier versions may work)
  • MySQL 4.1.22 or higher
  • mplayer
  • sox
  • lame
  • wget
  • Perl Modules: DBI, CGI, POSIX, MP3::Info, MP3::Tag
  • Optional: Apache w/PHP with phpMyAdmin
I've tried to keep everything packed into one large script for ease of installation, but there are a couple additional things: the SQL, a small config file, and a cronjob.

NOTE: To keep the vertical scrolling down I've collapsed the source of the longer files below. Click "expand source" to view.

Here's the SQL with test data. I intend to build a web-based interface for this at some point, but for now I just run apache and admin it with phpMyAdmin. I created a separate database called "streamshifter" and imported this SQL into it:
CREATE TABLE `streamshifter_config` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `program_name` varchar(24) NOT NULL DEFAULT 'streamshifter.cgi',
  `your_timezone` enum('AKT','AT','CT','ET','HT','MT','NT','PT','YT') NOT NULL DEFAULT 'ET' COMMENT 'Program scheduling times must be for this timezone.  Adjustments will be made automatically when traveling based on computer timezone.',
  `id3_timestamp_format` varchar(36) NOT NULL DEFAULT '' COMMENT 'If you want the timestamp included in the ID3 tags, please enter the format here.  Leave empty if you do not want the timestamp in the ID3 name.  Use MySQL anchors, see this for reference: http://dev.mysql.com/doc/refman/5.0/en/date-and-time-functions.htm',
  `path_to_cgibin` varchar(128) NOT NULL DEFAULT '',
  `path_to_library` varchar(255) NOT NULL DEFAULT '' COMMENT 'Full path to the library where you want to store recorded files.  Use trailing slash.',
  `add_to_itunes` enum('Yes','No') NOT NULL DEFAULT 'No' COMMENT 'Select "yes" if you''d like streamshifter to add the finished track to your itunes library.  This works *only* on Mac OS X, please select No for all other operating systems.',
  `auto_create_subdirs` enum('Yes','No') NOT NULL DEFAULT 'Yes' COMMENT 'Do you want it to auto-create subdirectories  in your library for recorded files?',
  `path_to_desktop` varchar(255) NOT NULL DEFAULT '' COMMENT 'Full path to your desktop, which is the default save location and where test files will be placed.  use trailing slash.',
  `path_to_rawdata` varchar(255) NOT NULL DEFAULT '' COMMENT 'Full path to temp file directory.  Temp files will be large, but will be removed after recording.  Use trailing slash.',
  `path_to_mplayer` varchar(128) NOT NULL DEFAULT '' COMMENT 'mplayer is required',
  `path_to_sox` varchar(128) NOT NULL DEFAULT '' COMMENT 'sox is required',
  `path_to_lame` varchar(128) NOT NULL DEFAULT '' COMMENT 'lame is required',
  `path_to_normalize` varchar(128) NOT NULL DEFAULT '' COMMENT 'normalize is required',
  `path_to_wget` varchar(128) NOT NULL DEFAULT '' COMMENT 'wget is required',
  `path_to_sendmail` varchar(128) DEFAULT NULL COMMENT 'If you want StreamShifter to send emails when there are new shows or errors with recording. ',
  `path_to_nice` varchar(128) DEFAULT NULL COMMENT 'The path to ''nice'' on your system.  CPU-intensive processes like lame, sox and normalize are niced.  Leave empty if you don''t want to use.',
  `max_recording_hours` int(2) NOT NULL DEFAULT '7',
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

-- 
-- Dumping data for table `streamshifter_config`
-- 

INSERT INTO `streamshifter_config` VALUES (1, 'streamshifter.cgi', 'PT', '%W, %M %e %h:%i %p %Z', '/User/user/bin/', '/Users/user/Music/Import/', 'Yes', 'Yes', '/Users/user/Desktop/', '/Users/user/tmp/', '/opt/local/bin/mplayer', '/opt/local/bin/sox', '/opt/local/bin/lame', '/opt/local/bin/normalize', '/opt/local/bin/wget', '/usr/sbin/sendmail', '/usr/bin/nice', 5, '2009-11-05 21:31:00');



CREATE TABLE `streamshifter_programs` (
  `id` int(11) NOT NULL auto_increment,
  `schedule_id` int(5) NOT NULL default '0',
  `title` varchar(255) NOT NULL default '',
  `recording_date` datetime NOT NULL default '0000-00-00 00:00:00',
  `recording_length` time NOT NULL default '00:00:00',
  `bitrate` int(4) NOT NULL default '0',
  `filesize` float NOT NULL default '0',
  `path_to_file` varchar(255) NOT NULL default '',
  `info_url` varchar(255) NOT NULL default '' COMMENT 'Can be a playlist',
  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`),
  KEY `path_to_file` (`path_to_file`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

CREATE TABLE `streamshifter_schedule` (
  `id` int(11) NOT NULL auto_increment,
  `user_id` int(3) NOT NULL default '0',
  `title` varchar(152) NOT NULL default '',
  `author` varchar(152) NOT NULL default '',
  `album_or_episode` varchar(152) default NULL,
  `composer_or_network` varchar(152) default NULL,
  `genre` enum('A Cappella','Acid','Acid Jazz','Acid Punk','Acoustic','Alternative','Alt. Rock','Ambient','Anime','Avantgarde','Ballad','Bass','Beat','Bebob','Big Band','Black Metal','Bluegrass','Blues','Booty Bass','BritPop','Cabaret','Celtic','Chamber Music','Chanson','Chorus','Christian Gangsta Rap','Christian Rap','Christian Rock','Classical','Classic Rock','Club','Club-House','Comedy','Contemporary Christian','Country','Crossover','Cult','Dance','Dance Hall','Darkwave','Death Metal','Disco','Dream','Drone','Drum & Bass','Drum Solo','Duet','Easy Listening','Electronic','Ethnic','Eurodance','Euro-House','Euro-Techno','Fast-Fusion','Folk','Folklore','Folk/Rock','Freestyle','Finance','Funk','Fusion','Game','Gangsta Rap','Goa','Gospel','Gothic','Gothic Rock','Grunge','Hardcore','Hard Rock','Heavy Metal','Hip-Hop','House','Humour','Indie','Industrial','Instrumental','Instrumental Pop','Instrumental Rock','Jazz','Jazz+Funk','JPop','Jungle','Latin','Lo-Fi','Meditative','Merengue','Metal','Musical','National Folk','Native American','Negerpunk','New Age','New Wave','News','Noise','Oldies','Opera','Other','Politics','Polka','Polsk Punk','Pop','Pop-Folk','Pop/Funk','Porn Groove','Power Ballad','Pranks','Primus','Progressive Rock','Psychedelic','Psychedelic Rock','Punk','Punk Rock','Rap','Rave','R&B','Reggae','Retro','Revival','Rhythmic Soul','Rock','Rock & Roll','Salsa','Samba','Satire','Showtunes','Ska','Slow Jam','Slow Rock','Sonata','Soul','Sound Clip','Soundtrack','Southern Rock','Space','Speech','Sports','Swing','Symphonic Rock','Symphony','Synthpop','Talk','Tango','Techno','Techno-Industrial','Terror','Thrash Metal','Top 40','Trailer','Trance','Tribal','Trip-Hop','Vocal') default NULL,
  `output_bitrate` enum('32','64','96','128','160','192','256','320') NOT NULL default '96',
  `adjust_url` enum('0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23') NOT NULL default '0' COMMENT 'The number of hours to adjust the date set in the date_formatted_url (to account for timezone differences)',
  `adjust_url_interval` enum('Future','Past') NOT NULL default 'Future' COMMENT 'If you want the date formatted url to be adjusted, need to specify whether it should be adjusted in the future or past.',
  `url` varchar(255) NOT NULL default '' COMMENT 'If the URL is date-specific and you want it to update on the day the program is scheduled, use date specifiers in place of the date/time.  See: http://dev.mysql.com/doc/refman/5.0/en/date-and-time-functions.html#function_date-format',
  `adjust_info_url` enum('0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23') NOT NULL default '0' COMMENT 'The number of hours to adjust the date set in the date_formatted_info_url (to account for timezone differences)',
  `adjust_info_url_interval` enum('Future','Past') NOT NULL default 'Future' COMMENT 'If you want the date formatted info url to be adjusted, need to specify whether it should be adjusted in the future or past.',
  `info_url` varchar(255) default NULL COMMENT 'Information about the program, or a playlist.  If the URL is date-specific (like, a playlist from a certain show) and you want it to update on the day the program is scheduled, use date specifiers in place of the date/time.  See: http://dev.mysql.com/doc/refman/5.0/en/date-and-time-functions.html#function_date-format',
  `repeat` enum('Once','Daily','Weekdays','Weekends','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday') NOT NULL default 'Daily',
  `hour` enum('00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23') NOT NULL default '00',
  `minute` enum('00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','32','33','34','35','36','37','38','39','40','41','42','43','44','45','46','47','48','49','50','51','52','53','54','55','56','57','58','59') NOT NULL default '00',
  `start_date` date NOT NULL default '0000-00-00',
  `duration_minutes` int(6) NOT NULL default '60',
  `save_to_library` enum('Yes','No') NOT NULL default 'Yes' COMMENT 'Save recorded file to library?  If no, will be saved to desktop.',
  `episodes_to_keep` enum('All','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','20','25','30','40') NOT NULL default '5',
  `add_to_itunes_playlists` varchar(255) NOT NULL default '' COMMENT 'If on a Mac with iTunes installed, you can specify what playlists to add this track to.  Separate multiple playlists with commas.  If a specified playlist doesn''t exist, it will be created.  Ignore this field if not on a Mac.',
  `itunes_bookmarkable` enum('Yes','No') NOT NULL default 'Yes' COMMENT 'Set to "Yes" if you''re on a mac and you want streamshifter to set the track to bookmarkable (iTunes remembers the last playback position.  Handy for long programs.  Works only on Mac OS X.  Set to "No" for other operating systems.',
  `last_run` datetime NOT NULL default '0000-00-00 00:00:00',
  `last_run_result` enum('Success','Fail') default NULL,
  `last_run_comments` text,
  `active` enum('Yes','No') NOT NULL default 'Yes',
  PRIMARY KEY  (`id`),
  KEY `episodes_to_keep` (`episodes_to_keep`),
  KEY `user_id` (`user_id`),
  KEY `repeat` (`repeat`,`hour`,`minute`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=6 ;

INSERT INTO `streamshifter_schedule` VALUES (1, 1, 'This American Life', 'Ira Glass', '', 'WUIS', 'Other', '128', '0', 'Future', 'http://wuis.streamguys.net/wuis64.m3u', '0', 'Future', 'http://www.thislife.org', 'Friday', '17', '00', '2008-07-01', 60, 'Yes', '3', '0pods,this american life', 'Yes', '2009-06-19 18:02:53', 'Success', 'New episode of This American Life - Friday, June 19 05:00 PM PDT successfully recorded.  Check it out!\n\nFile: /Home/user/Music/Import/this_american_life/This_American_Life_2009-06-19-17-00-01.mp3\n\nDuration: 01:00:03', 'Yes');
INSERT INTO `streamshifter_schedule` VALUES (2, 1, 'Variations', 'DJG', NULL, 'KBCS', 'Reggae', '128', '0', 'Future', 'http://ophanim.net/cast/tunein.php/kbcs96/playlist.pls', '0', 'Future', NULL, 'Friday', '23', '00', '0000-00-00', 120, 'Yes', '5', '', 'Yes', '2009-06-20 01:07:16', 'Success', 'New episode of Variations - Friday, June 19 11:00 PM PDT successfully recorded.  Check it out!\n\nFile: /Home/user/Music/Import/variations/Variations_2009-06-19-23-00-01.mp3\n\nDuration: 02:00:41', 'Yes');
INSERT INTO `streamshifter_schedule` VALUES (3, 1, 'Philosophy Talk', 'Ken Taylor & John Perry', '', 'KALW', 'Talk', '128', '0', 'Future', 'http://www.kalw.org/media/Streaming%20Links/kalw-mp3.pls', '0', 'Future', '', 'Sunday', '10', '07', '0000-00-00', 53, 'Yes', '5', '0pods,philosophy talk', 'Yes', '2009-06-21 11:02:59', 'Success', 'New episode of Philosophy Talk - Sunday, June 21 10:07 AM PDT successfully recorded.  Check it out!\n\nFile: /Home/user/Music/Import/philosophy_talk/Philosophy_Talk_2009-06-21-10-07-01.mp3\n\nDuration: 00:53:45', 'Yes');
INSERT INTO `streamshifter_schedule` VALUES (4, 1, 'Evan "Funk" Davies Show', 'Evan Davies', '', 'WFMU', 'Other', '128', '9', 'Past', 'http://mp3archives.wfmu.org/archive/kdb/mp3jump.mp3/0:8:41/0/ED/ed%y%m%d.mp3', '0', 'Future', 'http://wfmu.org/playlists/ED', 'Wednesday', '06', '00', '0000-00-00', 180, 'Yes', '3', 'radio', 'Yes', '2009-06-24 06:05:08', 'Success', 'New episode of Evan ''Funk'' Davies Show - Wednesday, June 24 06:00 AM PDT successfully recorded.  Check it out!\n\nFile: /Home/user/Music/Import/evan_funk_davies_show/Evan_Funk_Davies_Show_2009-06-24-06-00-01.mp3\n\nDuration: 03:07:18', 'Yes');
INSERT INTO `streamshifter_schedule` VALUES (5, 1, 'Stochastic Hit Parade', 'Bethany Ryker', '', 'WFMU', 'Other', '128', '11', 'Past', 'http://mp3archives.wfmu.org/archive/kdb/mp3jump.mp3/0:8:7/0/HP/hp%y%m%d.mp3', '0', 'Future', 'http://wfmu.org/playlists/HP', 'Monday', '06', '00', '0000-00-00', 120, 'Yes', '5', 'radio,stochastic hit parade', 'Yes', '2009-06-22 06:05:09', 'Success', 'New episode of Stochastic Hit Parade - Monday, June 22 06:00 AM PDT successfully recorded.  Check it out!\n\nFile: /Home/user/Music/Import/stochastic_hit_parade/Stochastic_Hit_Parade_2009-06-22-06-00-01.mp3\n\nDuration: 02:07:52', 'Yes');

CREATE TABLE `streamshifter_users` (
  `id` int(5) NOT NULL auto_increment,
  `username` varchar(24) NOT NULL default '',
  `password` varchar(24) NOT NULL default '',
  `name` varchar(128) NOT NULL default '',
  `email` varchar(128) NOT NULL default '',
  `user_type` enum('admin','user') NOT NULL default 'user',
  `email_alerts` enum('Yes','No') NOT NULL default 'Yes' COMMENT 'Select "Yes" if you want to be alerted of new StreamShift recordings or about errors with recording',
  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`),
  KEY `username` (`username`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

INSERT INTO `streamshifter_users` VALUES (1, 'user', 'yourpassword', 'Your Name', 'you@you.com', 'admin', 'Yes', '2008-08-06 08:00:30');
There's a small configuration file which the script needs. Because it will contain MySQL connection info (edit the file with your specific info) you're going to want to put it in a safe place with restrictive permissions. I call the file "streamshifter.config":
# MYSQL database connection information
MYSQL_HOST: localhost
MYSQL_DATABASE: streamshifter 
MYSQL_USER: user
MYSQL_PASSWORD: password

# LOGGING: 0=Off 1=On
LOGGING: 1

# admin's email address
WEBMASTER: you@you.com
The only editing of the script itself you need to do is edit the path to the configuration file. And chmod 755 it of course. I call it "streamshifter.cgi":
#!/usr/bin/perl

my $path_to_config = '/Applications/streamshifter.config';

use DBI;
use CGI;
use strict;
use POSIX qw(strftime);
require MP3::Info;
require MP3::Tag;

# get the streamshifter configuration
my @conf_errors = ();
my ($host,$database,$user,$mysqlpassword,$webmaster,$logging,$conf_errors_ref) = &getDbConfig($path_to_config);
if ($conf_errors_ref) { @conf_errors = @$conf_errors_ref; }
# errors, at this point, are fatal
if ($conf_errors[0]) {
 my $error = join(". ",@conf_errors);
 print "ERRORS: $error.\n";
 exit 1;
}

# in our temp dir, we need to clean up files older than x days
# there shouldn't be any, but just in case there was a crash
my $max_file_age = 2;

# set up the db connection
my $dbh = DBI->connect("DBI:mysql:database=$database;host=$host","$user","$mysqlpassword",{'RaiseError'=>1}) or die "Unable to connect: $DBI::errstr\n"; 

# see if there are arguments...(flags on the command line)
my ($errors_ref,$spec_id,$action,$rec_url,$rec_minutes) = "";
my @errors = ();
if ($ARGV[0]) {
 ($errors_ref,$spec_id,$action,$rec_url,$rec_minutes) = checkFlags(\@ARGV,$dbh);
 if ($errors_ref) { @errors = @$errors_ref; }
 if ($errors[0]) {
  my $error = join(". ",@errors);
  print "ERRORS: $error.\n";
  exit 1;
 }
}

# get more configuration info, from the db
my @config_errors = ();
my ($self,$your_timezone,$id3_timestamp_format,$script_name,$path_to_cgibin,
 $path_to_library,$add_to_itunes,$auto_create_subdirs,$path_to_desktop,$path_to_rawdata,
 $mplayer,$sox,$normalize,$lame,$wget,$sendmail,$nice,$max_recording_hours,$config_errors_ref) = &get_config($dbh);
if ($config_errors_ref) { @config_errors = @$config_errors_ref; }
if ($config_errors[0]) {
 my $error = join(". ",@config_errors);
 print "ERRORS: $error.\n";
 exit 1;
}

# get our date vars - we start the recording 1 minute earlier so we can test the url and throw an error if it fails
my ($date,$hour,$minute,$day,$year,$ts,$ts2,$date_short,$timezone) = &getDate($your_timezone);

# logfile
my $logfile_name = 'radio_log_' . $date . '.txt'; #do not change this filename!
my $logfile = $path_to_rawdata . $logfile_name;
my $delete_after = 2; # delete old logfiles older than this number of days

# check if the logfile needs rotated
&rotateFile($path_to_rawdata,$delete_after);



# the 'check' flag performs our routine maintenance and checks for scheduled programs
if ($action eq 'check') {
 
 # check for new programs, do routine maintenance.  we don't return from this
 #&log($spec_id,"check","checking",$logfile,$logging);
 &checkSchedule($self,$max_file_age,$path_to_rawdata,$max_recording_hours,$hour,$minute,$day,$date,$path_to_library,$auto_create_subdirs,$add_to_itunes,$dbh);
 
} elsif (($action eq 'record') || ($action eq 'test')) {

 # load up the program's info
 my ($path,$temp_file,$final_file,$temp_filepath,$file_filepath,
  $final_filepath,$ext,$user_id,$title,$author,$album,$composer,$genre,$url,
  $adjust_url,$adjust_url_interval,$info_url,$adjust_info_url,
  $duration_min,$duration_sec,$output_bitrate,$add_to_itunes_playlists,$itunes_bookmarkable,$adjust_url,$username,
  $contact_name,$contact_email,$email_alerts) = &getProgramInfo($spec_id,$action,$path_to_library,$path_to_desktop,$rec_url,$rec_minutes,$id3_timestamp_format,$dbh);

 # figure out what kind of url this is
 my ($success,$khz,$cmd,$get_prog,$temp_filepath,$ext,$failreason) = &urlGetMethod($url,$temp_filepath,$mplayer,$nice,$wget,$ext,$path_to_rawdata);
 if ($success == 0) {
  my $message = "\n\nCould not retrieve data from URL $url.\n\n$failreason";
  &log($spec_id,$title,$message,$logfile,$logging);
  &fail($spec_id,$temp_filepath,"$message");
  if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
  exit 1;
 } else {
  &log($spec_id,$title,"$url successfully tested",$logfile,$logging);
 }
 
 # fork for the recording process
 my $pid = fork();
 
 # start the actual recording
 if ($pid == 0) {
  &log($spec_id,$title,"starting retrieval of $url with $get_prog (pid:$pid). executing $cmd \n",$logfile,$logging);
     exec $cmd;
 } # end if for child
 
 # parent process (this script).  we wait the duration of the program then kill the child
 elsif ($pid > 0) {
  
  if ($get_prog eq 'wget') {
   
   &wgetProgram($pid,$path,$spec_id,$title,$wget,$ext,$temp_filepath,$file_filepath,$final_filepath,$email_alerts,$sendmail,$contact_email,$contact_name,$duration_sec,$duration_min);
        
  } else {
   
   &mplayerProgram($pid,$path,$spec_id,$title,$khz,$mplayer,$sox,$normalize,$lame,$nice,$url,$ext,$temp_filepath,$file_filepath,
    $final_file,$final_filepath,$email_alerts,$sendmail,$contact_email,
    $contact_name,$duration_sec,$duration_min,$title,$author,$composer,$output_bitrate,
    $album,$year,$info_url,$genre,$date);
    
  } # end if /else for wget or mplayer 
  
  # tag our file
  if (-e $file_filepath) {
   &tagFile($title,$author,$composer,$year,$album,$file_filepath,$info_url,$genre);
  }
  
  # delete temp file and finish
  &cleanAndFinish($spec_id,$title,$file_filepath,$final_filepath,$get_prog,$temp_filepath,$email_alerts,$sendmail,$contact_email,$contact_name);
  
  # final check and completion
  if (-e $final_filepath) {
   # success!
   # if they want to auto-add stuff to itunes
   if ($add_to_itunes eq 'Yes') {
    my $return = &iTunes($final_filepath,$add_to_itunes_playlists,$add_to_itunes,$itunes_bookmarkable);
   }
   
   &success($spec_id,$final_filepath,$title,$info_url,$contact_email,$contact_name,$email_alerts,$action);
  } else {
   &fail($spec_id,"");
   my $message = "Final mp3 file wasn't found, problem moving from data directory to save location, $file_filepath -> $final_filepath";
   &log($spec_id,$title,$message,$logfile,$logging);
   if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
   &fail($spec_id,$temp_filepath,"$message");
   exit 1;
  }
 
 } # end if for parent
 
 # our fork() has failed, no child process is created!  this should obviously never happen
 elsif ($pid < 0) {
  my $message = "fork() failed... can not create child process to start mplayer";
  &log($spec_id,$title,$message,$logfile,$logging);
  if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
  &fail($spec_id,$temp_filepath,"$message");
  exit 1;
 }   
 
 my $ts_end = strftime( "%Y%m%d%H%M%S",localtime(time));
 &log($spec_id,$title,"ended recording of $title at $ts_end: $final_filepath",$logfile,$logging);
 

} else {

 print "no action passed\n";
 
}# end if/else for NO action (flags)

$dbh->disconnect();

######################### SUBROUTINES #########################

#################################################################################################
###################            CONFIGURATION-RELATED SUBROUTINES      #####################
#################################################################################################

sub getDbConfig {

 my $path_to_config = shift;
 
 my ($host,$database,$user,$mysqlpassword,$webmaster,$logging) = "";
 my @errors = ();
 
 # first check that the config exists
 if (not(-e $path_to_config)) {
  print "ERROR: Database configuration file not found\n";
  exit 1;
 }
 
 open(FILE,"$path_to_config");
 my @lines = ;
 close(FILE);
 
 foreach my $line (@lines) {
 
  if ($line =~ /^MYSQL_HOST:(.*?)$/) {
   $host = $1;
   $host = &trim($host);
  }
  elsif ($line =~ /^MYSQL_DATABASE:(.*?)$/) {
   $database = $1;
   $database = &trim($database);
  }
  elsif ($line =~ /^MYSQL_USER:(.*?)$/) {
   $user = $1;
   $user = &trim($user);
  }
  elsif ($line =~ /^MYSQL_PASSWORD:(.*?)$/) {
   $mysqlpassword = $1;
   $mysqlpassword = &trim($mysqlpassword);
  }
  elsif ($line =~ /^LOGGING:(.*?)$/) {
   $logging = $1;
   $logging = &trim($logging);
  }
  elsif ($line =~ /^WEBMASTER:(.*?)$/) {
   $webmaster = $1;
   $webmaster = &trim($webmaster);
  } else {
   #
  }

 } # end foreach thru lines of config file
 
 # make sure we have all our config info
 if (!$host) {
  push(@errors,"MYSQL host not found in config file");
 }
 if (!$database) {
  push(@errors,"MYSQL database not found in config file");
 }
 if (!$user) {
  push(@errors,"MYSQL user not found in config file");
 }
 if (!$mysqlpassword) {
  push(@errors,"MYSQL password not found in config file");
 }
 if (!$webmaster) {
  push(@errors,"Webmaster's email not found in config file");
 } else {
  # validate the email
  my $msg = &is_valid_email($webmaster);
  if (!$msg) {
   push(@errors,"Webmaster's email not formatted correctly: $msg");
  }
 }
 
 return ($host,$database,$user,$mysqlpassword,$webmaster,$logging,\@errors);
 
} # end sub getDbConfig

sub get_config {
 # get the streamshifter config stuff from the database

 my @errors = ();
 my $sql = "SELECT * FROM streamshifter_config LIMIT 1";
 my $sth = &db_query($sql);
 my ($self,$your_timezone,$id3_timestamp_format,$script_name,$path_to_cgibin,$path_to_library,$add_to_itunes,$auto_create_subdirs,$path_to_desktop,$path_to_rawdata,$mplayer,$sox,$normalize,$lame,$wget,$sendmail,$nice,$max_recording_hours) = "";
 while (my $ref = $sth->fetchrow_hashref) {
  $script_name=$ref->{'program_name'};
  $your_timezone=$ref->{'your_timezone'};
  $id3_timestamp_format=$ref->{'id3_timestamp_format'};
  $path_to_cgibin=$ref->{'path_to_cgibin'};
  $path_to_library=$ref->{'path_to_library'};
  $add_to_itunes=$ref->{'add_to_itunes'};
  $auto_create_subdirs=$ref->{'auto_create_subdirs'};
  $path_to_desktop=$ref->{'path_to_desktop'};
  $path_to_rawdata=$ref->{'path_to_rawdata'};
  $mplayer=$ref->{'path_to_mplayer'};
  $sox=$ref->{'path_to_sox'};
  $normalize=$ref->{'path_to_normalize'};
  $lame=$ref->{'path_to_lame'};
  $wget=$ref->{'path_to_wget'};
  $sendmail=$ref->{'path_to_sendmail'};
  $nice=$ref->{'path_to_nice'} . " ";
  $max_recording_hours=$ref->{'max_recording_hours'};
 }
 $sth->finish();
 $self = $path_to_cgibin . $script_name;
 
 # ERROR CHECKING
 
 # see if we have our directories and they exist
 if ((!$path_to_cgibin) || (not(-d $path_to_cgibin))) {
  push(@errors,"Path to cgi-bin not found, or path is not a directory: $path_to_cgibin");
 } 
 if ((!$self) || (not(-e $self))) {
  push(@errors,"Full path to program not found: $self");
 } 
 if ((!$path_to_library) || (not(-d $path_to_library))) {
  push(@errors,"Path to library not found, or path is not a directory: $path_to_library");
 } 
 if ((!$path_to_desktop) || (not(-d $path_to_desktop))) {
  push(@errors,"Path to desktop not found, or path is not a directory: $path_to_desktop");
 } 
 if ((!$path_to_rawdata) || (not(-d $path_to_rawdata))) {
  push(@errors,"Path to temp file dir not found, or path is not a directory: $path_to_rawdata");
 } 
 
 # if we don't have the program paths at this point, check the system
 if ((!$mplayer) || (not(-e $mplayer))) { $mplayer = stripSystemResponse(`/usr/bin/which mplayer`); }
 if ((!$sox) || (not(-e $sox))) { $sox = stripSystemResponse(`/usr/bin/which sox`); }
 if ((!$normalize) || (not(-e $normalize))) { $normalize = stripSystemResponse(`/usr/bin/which normalize`); }
 if ((!$lame) || (not(-e $lame))) { $lame = stripSystemResponse(`/usr/bin/which lame`); }
 if ((!$wget) || (not(-e $wget))) { $wget = stripSystemResponse(`/usr/bin/which wget`); }
 
 # if we don't have at least mplayer, wget and lame at this point, there's not much we can do
 if ((!$mplayer) || (not(-e $mplayer))) {
  push(@errors,"mplayer not found, or path is invalid: $mplayer");
 }
 if ((!$sox) || (not(-e $sox))) {
  push(@errors,"sox not found, or path is invalid: $sox");
 }
 if ((!$normalize) || (not(-e $normalize))) {
  push(@errors,"normalize not found, or path is invalid: $normalize");
 }
 if ( (!$lame) || (not(-e $lame)) ) {
  push(@errors,"lame not found, or path is invalid: $lame");
 }
 
 if ((!$wget) || (not(-e $wget))) {
  push(@errors,"wget not found, or path is invalid: $wget");
 }
 
 if ( ($add_to_itunes eq 'Yes') && (!checkForDarwin() )) {
  push(@errors,"Doesn't appear you're running darwin, so please select 'no' for 'add_to_itunes' in configuration.");
 }
 
 return ($self,$your_timezone,$id3_timestamp_format,$script_name,$path_to_cgibin,$path_to_library,$add_to_itunes,$auto_create_subdirs,$path_to_desktop,$path_to_rawdata,$mplayer,$sox,$normalize,$lame,$wget,$sendmail,$nice,$max_recording_hours,\@errors);

} # end sub get configuration

#################################################################################################
###################            SCHEDULE-RELATED SUBROUTINES        #####################
#################################################################################################

sub checkFlags {
 # checks command-line flags
 
 my $args_ref = shift;
 @ARGV = @$args_ref;
 
 my $arguments = @ARGV;
 my @valid_args = qw(-t -m -r -s -u);
 
 my ($test_id,$rec_id,$rec_url,$rec_minutes) = 0;
 my $hasvalid = 0;
 
 my $action = 'record';

 # check the values of the flags
 for (my $i=0; $i<$arguments; $i++) {
  # if the -t "test" flag
  if ($ARGV[$i] eq '-t') {
   $hasvalid = 1;
   if ($ARGV[$i+1]) {
    if ($ARGV[$i+1] =~ /^\d+$/) {
     $test_id = $ARGV[$i+1];
     $action = 'test';
    } else {
     push(@errors,"Value for -t (test) flag must be a positive integer - the ID of the record in the database");
    }
   } else {
    push(@errors,"The -t (test) flag needs a value - the ID of the program in the database");
   }
  } elsif ($ARGV[$i] eq '-r') {
   # if it's the "record" flag
   if ($ARGV[$i+1]) {
    $hasvalid = 1;
    if ($ARGV[$i+1] =~ /^\d+$/) {
     $rec_id = $ARGV[$i+1];
     $action = 'record';
    } else {
     push(@errors,"Value for -r (record) flag must be a positive integer - the ID of the record in the database");
    }
   } else {
    push(@errors,"The -r (record) flag needs a value - the ID of the program in the database");
   }
  } elsif ($ARGV[$i] eq '-u') {
   # it's the "URL" flag
   if (&validateURL($ARGV[$i+1])) {
    $rec_url = $ARGV[$i+1];
   } else {
    push(@errors,"Argument after -u (URL) flag must be a properly-formatted URL beginning with 'http' or 'mms'.  If you do not include this flag the default is the URL for the passed program ID in the database");
   }
  } elsif ($ARGV[$i] eq '-m') {
   # it's the "minutes" flag
   if ($ARGV[$i+1] =~ /^\d+$/) {
    $rec_minutes = $ARGV[$i+1];
   } else {
    push(@errors,"Value for -m (duration) flag must be a positive integer - number of minutes to record stream.  If you do not include this flag the default is 1");
   }
  } elsif ($ARGV[$i] eq '-s') {
   # it's the "check schedule" flag
   $hasvalid = 1;
   $action = 'check';
   #print "checking\n";
   #my $output = `/bin/echo \$PERL5LIB`;
   #print $output;
   
  } else {
   #
  }
 } # end for loop
 
 # if there's a url flag but no record or test flag, throw error
 if (($rec_url) && (!$action)) {
  push(@errors,"The -u (URL) flag must be used with one of -t (test) or -r (record) flags with the ID of the program in the database");
 }
 
 # if no flag found, error
 if ($hasvalid == 0) {
  push(@errors,"\nValid flags not found.  Valid flags:\n-s\tcheck schedule for recorded programs\n-r\trecord (next argument is the ID of program in the database)\n-t\ttest (next argument is the ID of program in the database)\n-m\tminutes to test (Optional. Next argument is number of minutes for testing. Used with -t. Default is 1.)\n-u\tURL of stream (Optional. Used with -r. Default is the URL for the program in the database.)\n");
 }
 
 # if test flag is set and valid but there's no minute flag, make minute 1
 if (($test_id) && (!$rec_minutes)) {
  $rec_minutes = 1;
 }
 
 my $id = 0;
 if ($test_id) { 
  $id = $test_id; 
 }
 if ($rec_id) { 
  $id = $rec_id;
 }
 
 # make sure the id exists in the db
 if ($id) {
  my $sql = "SELECT COUNT(*) FROM streamshifter_schedule WHERE id = '$id'";
  my $sth = &db_query($sql,$dbh);
  my $count = 0;
  while (my $ref = $sth->fetchrow_arrayref) {
   $count = $$ref[0]
  } # end while loop
  
  if ($count < 1) {
   push(@errors,"No corresponding program found for ID specified");
  }
 }
 
 return (\@errors,$id,$action,$rec_url,$rec_minutes);
 
} # end sub checkFlags

sub cleanTemp {
 # cleans any old temp files

 my ($max_file_age,$path_to_rawdata) = @_;
 opendir(DIR, $path_to_rawdata) || print "can't opendir $path_to_rawdata: $! to delete old files";
 my @files = readdir(DIR);
 closedir(DIR);
 shift(@files);
 shift(@files);
 foreach my $file (@files) {
  my $thisfile = $path_to_rawdata . $file;
  my $file_age = (-M $thisfile);
  if (($file_age > $max_file_age) && (-f $thisfile)) {
   unlink($thisfile);
   #print "deleting $thisfile because it is over $max_file_age days old\n";
  }
 }

} # sub cleanTemp

sub checkSchedule {
 # checks the database for programs scheduled to run
 
 my ($self,$max_file_age,$path_to_rawdata,$max_recording_hours,$hour,$minute,$day,$date,$path_to_library,$auto_create_subdirs,$add_to_itunes,$dbh) = @_;
 
 #print "checking schedule for $hour,$minute,$day,$date\n";

 &cleanTemp($max_file_age,$path_to_rawdata);
 
 # clean our program table/library of old or orhpaned program recordings
 &cleanPrograms($path_to_library,$auto_create_subdirs,$add_to_itunes,$script_name);
 
 # check for any zombie mplayer processes.  If so, kill them
 &checkZombie($max_recording_hours);
 
 my $programs_ref = &checkPrograms($hour,$minute,$day,$date);
 my @program_ids = @$programs_ref;
 
 # if array is empty, exit
 my $program_count = @program_ids;
 if ($program_count < 1) {
  exit 0;
 } else {
  # we have at least one program.  we SHOULD be be forking off record processes for each one but for SOME reason fork is causing me grief.  Instead we are forking off calls to this program with the record flag set, which does the trick
  foreach my $program (@program_ids) {
   my $program_pid = fork();
   # child process
   if ($program_pid == 0) {
    my $mycmd = "$self -r $program";
    &log($spec_id,"n/a","executing: $mycmd",$logfile,$logging);
    exec("$mycmd");
   } # end if for child 
  } # end loop thru programs
 } # end if/else for program count
 
 exit 0;
 
} # end sub checkSchedule

sub checkPrograms {
 # related to the checkSchedule sub

 my ($hour,$minute,$day,$date) = @_;
 # find any programs that may need to be recorded
 my @program_ids = ();
 my $sql0 = "SELECT * FROM streamshifter_schedule WHERE active = 'Yes' AND hour = '$hour' AND minute = '$minute'";
 #print "$sql0\n";
 my $sth0 = &db_query($sql0);
 while (my $ref0 = $sth0->fetchrow_hashref) {
  
  # check if the program is scheduled for TODAY
  my $record_program = &checkDay($ref0->{'repeat'},$day,$ref0->{'start_date'},$date);
  
  if ($record_program) {
   push(@program_ids,$ref0->{'id'});
   &log($ref0->{'id'},"n/a","found a program id to record: $ref0->{'id'}",$logfile,$logging);
  }
 }
 $sth0->finish();
 
 return (\@program_ids);

} # end sub checkPrograms

sub getDate {
 # get the date in the formats we need it.  We use mysql because it's waaaaaaay easier
 
 # we have to watch for timezones.  check timezone of schedule vs the tz we're in and adjust our date-getting accordingly
 my $input_timezone = shift;
 my ($adjust_min,$your_timezone) = &doTimezone($input_timezone);
 #print "adjust: $adjust_min\n";
 my $date_adj = "DATE_ADD";
 if ($adjust_min < 0) {
  $date_adj = "DATE_SUB";
  $adjust_min = ($adjust_min*-1);
 }
 
 my ($date,$hour,$minute,$day,$year,$ts,$ts2,$date_short);
 my $day_q = "
  SELECT 
  DATE_FORMAT(DATE_ADD(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE), INTERVAL 1 MINUTE),'%H%i') AS HHMM,
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%W') as DAY, 
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%H') as HOUR,
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%i') AS MINUTE, 
  DATE_FORMAT(${date_adj}(DATE_ADD(NOW(), INTERVAL $adjust_min MINUTE), INTERVAL 1 MINUTE),'%Y-%m-%d-%H-%i-%s') as TS, 
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%Y-%m-%d') as TODAY, 
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%Y') as YEAR, 
  DATE_FORMAT(${date_adj}(NOW(), INTERVAL $adjust_min MINUTE),'%c/%e/%y') AS DATE_SHORT
  ";
 #print "$day_q\n";
 my $sth_date = &db_query($day_q);
 while (my $ref = $sth_date->fetchrow_hashref) {
  $day = $ref->{'DAY'};
  ($hour,$minute) = $ref->{'HHMM'} =~ /^(\d\d)(\d\d)$/;
  #print "HOUR: $hour MINUTE: $minute\n";
  $date = $ref->{'TODAY'};
  $year = $ref->{'YEAR'};
  $ts = $ref->{'TS'};
  $ts2 = strftime( "%Y-%m-%d-%H-%M",gmtime(time)); # this is for the filename, kept in GMT so it's the same when music libraries in multiple timezones are synced
  $date_short = $ref->{'DATE_SHORT'};
 }
 $sth_date->finish();
 
 #my $timezone = strftime( "%Z",localtime(time));
 #&log('n/a','n/a',"home timezone: $your_timezone, current timezone: $timezone $date_adj $adjust_min",$logfile,$logging);
 return ($date,$hour,$minute,$day,$year,$ts,$ts2,$date_short,$your_timezone);

} # end sub getDate

sub doTimezone {
 # check the scheduled program's timezone vs. what the computer says the timezone we are IN is
 # allows the programs to record at the proper time when we are traveling
 
 my $sched_timezone = shift;
 my $timezone = strftime( "%Z",localtime(time));
 my %zones = (
  'AKST' => '9',
  'AST' => '4',
  'AKDT' => '8',
  'ADT' => '3',
  'CST' => '6',
  'CDT' => '5',
  'EST' => '5',
  'EDT' => '4',
  'HST' => '10',
  'MST' => '7',
  'MDT' => '6',
  'NST' => '3.5',
  'NDT' => '2.5',
  'PST' => '8',
  'PDT' => '7',
  'YST' => '8'
 );

 # hack on the schedule timezone to give it a proper timezone code
 $timezone =~ /([\w]{2})$/;
 my $suffix = $1;
 my $code = $sched_timezone;
 $code =~ s/[\w]$//;
 $code .= $suffix;
 
 my $utc_current = &round($zones{$timezone}*60);
 my $utc_sched = &round($zones{$code}*60);
 my $adjust_minute = ($utc_current-$utc_sched);
 
 return ($adjust_minute,$code);
 
} # end sub timezone

sub checkZombie {
 # checks for mplayer processes that have run for too long

 my $kill_after_hours = shift;
 my $max_time = $kill_after_hours*60*60;
 my @ps = `/bin/ps ax -o pid,etime,command | /usr/bin/grep mplayer`;
 chomp(@ps);
 
 foreach my $cur_proc (@ps) {
 
  # the fields of the processes (as names)
  my ($pid,$elapsed,$cmd) = $cur_proc =~ /^\s*(\d+)\s+([-:\d]+)\s+(.*)$/;
  $elapsed = &trim($elapsed);
  my ($h,$m,$s);
  if ($elapsed =~ /(\d\d):(\d\d):(\d\d)$/) {
   ($h,$m,$s) = ($1,$2,$3);
  } else {
   $elapsed =~ /(\d\d):(\d\d)$/;
   ($h,$m,$s) = (0,$1,$2);
  }

  # elapsed time in seconds
  my $eseconds = $h*60*60 + $m*60 + $s;

  # if we find an mplayer process greater than $kill_after_hours, we need to kill it
  if ($eseconds > $max_time) {
   # kill("15",$pid);
   my $kill = `/bin/kill -15 $pid`;
   &log("n/a","n/a","Sent a TERM signal to a zombie mplayer process pid $pid because it was running for $eseconds process: $cur_proc",$logfile,$logging);
  }
 
 } # end for thru processes

} # end sub checkzombie

sub cleanPrograms {
 # cleans records and info files of programs that are no longer in the filesystem
 my ($path_to_library,$auto_create_subdirs,$add_to_itunes,$script_name) = @_;
 
 # array to contain IDs of programs we need to delete
 my @orphaned = ();

 # first let's delete any records that don't have files
 my $sql = "SELECT id,path_to_file FROM streamshifter_programs";
 my $sth = &db_query($sql);
 while (my $ref = $sth->fetchrow_arrayref) {
  
  my $record = $$ref[0];
  my $program_path = $$ref[1];
  
  # check if the program is still on the filesystem
  if (not(-e $program_path)) {
   &log($record,"n/a","could not find file for recorded program, going to remove record: $record missing file: $program_path",$logfile,$logging);
   push(@orphaned,$record);
  }
 
 } # end while thru program recordings
 $sth->finish();
 
 # now let's query the schedule and see if any of our programs have more than their allotted number of episodes
 my $sql = "SELECT id,episodes_to_keep,title,add_to_itunes_playlists,itunes_bookmarkable FROM streamshifter_schedule";
 my $sth = &db_query($sql);
 while (my $ref = $sth->fetchrow_arrayref) {
  
  my $program_id = $$ref[0];
  my $episodes = $$ref[1];
  my $progtitle = $$ref[2];
  my $add_to_itunes_playlists = $$ref[3];
  my $itunes_bookmarkable = $$ref[4];
  
  # if episodes != \d then make the variable an impossibly high number so no files ever get expired
  if (not($episodes =~ /^\d/)) {
   $episodes = 100000;
  }
  
  # now look at each program and see how many episodes it has
  my $sql2 = "SELECT path_to_file FROM streamshifter_programs WHERE schedule_id = '$program_id'";
  my $count = 0;
  my @recorded = ();
  my $sth2 = &db_query($sql2);
  while (my $ref2 = $sth2->fetchrow_arrayref) {
   push(@recorded,$$ref[2]);
   #print "$$ref2[0]\n";
  }
  $count = @recorded;
  $sth2->finish();
  
  # if count is greater than number of allotted programs, sort by newest and delete the older ones over the limit
  if ($count > $episodes) {
   my $sql3 = "SELECT id,path_to_file,timestamp,path_to_file FROM streamshifter_programs WHERE schedule_id = '$program_id' ORDER BY timestamp DESC";
   my $sth3 = &db_query($sql3);
   my $pr_counter = 0;
   while (my $ref3 = $sth3->fetchrow_arrayref) {
    $pr_counter++;
    my $del_id = $$ref3[0];
    my $del_path = $$ref3[1];
    my $del_timestamp = $$ref3[2];
    my $fullpathtofile = $$ref3[3];
    if ($pr_counter > $episodes) {
     
     # if there are playlists specified, we need to delete the track from those playlists
     # ( to keep the lists clean )
     if ($add_to_itunes_playlists) {
      my $del_return = &playlistTrackDeleteCheck($program_id,$fullpathtofile,$add_to_itunes_playlists,$logfile,$logging);
     }
     
     push(@orphaned,$del_id);
     &log($program_id,"n/a","Program id $program_id has a max of $episodes episodes. Deleting older episode with id $del_id ($del_timestamp) and corresponding file",$logfile,$logging);
     if (unlink($del_path)) {
      &log($program_id,"n/a","deleted $del_path",$logfile,$logging);
     } else {
      &log($program_id,"n/a","Could not delete $del_path - it was probably deleted by other means",$logfile,$logging);
     }
    } # end if for counter > episodes
   } # end while thru this program
   $sth3->finish();   
   
  } # end if for count is greater than allowed episodes
  
  # if we have auto-created subdirs in the library (a subdirectory for each program), 
  # lets loop thru that directory in the filesystem and make sure that we have only our newest n
  # programs in there.  The reason I do this is sometimes I add files that were created elsewhere
  # (another campaign manager, wiretap pro, a podcast mp3, etc.)
  my $tempname = lc(&replaceSpace(&stripDateSpec($progtitle)));
  my $dir = $path_to_library . $tempname . '/';
  if (($auto_create_subdirs eq 'Yes') && (-d $dir)) {
   my @filelist = ();
   #open the directory
   opendir(DH, $dir) || die "Can't open directory $dir: $!\n"; 
   #sort them by creation date, newest file first, shove into array
   @filelist = sort { -M "$dir/$a" <=>  -M "$dir/$b" } grep { -f "$dir/$_" && /\.[MmOo][PpGg][3Gg]$/ } readdir DH;
   closedir(DH);
   
   # loop thru the files and mark any excess ones for deletion
   my $filecounter = 1;
   foreach my $file(@filelist) {
    my $fullpathtofile = $dir . $file;
    if ($filecounter > $episodes) {
     
     # if there are playlists specified, we need to delete the track from those playlists
     # ( to keep the lists clean )
     if ($add_to_itunes_playlists) {
      my $del_return = &playlistTrackDeleteCheck($program_id,$fullpathtofile,$add_to_itunes_playlists,$logfile,$logging);
     }
     
     # over the limit - DELETE and REMOVE FROM DB (doubtful it's in there, but just to be sure)
     if (unlink($fullpathtofile)) {
      &log($program_id,"n/a","Deleted $fullpathtofile because it was in the program dir and is number $filecounter when only $episodes are allowed",$logfile,$logging);
     } else {
      &log($program_id,"n/a","problem deleting $fullpathtofile",$logfile,$logging);
     }
     my $delquery = "DELETE FROM streamshifter_programs WHERE path_to_file = '$fullpathtofile'";
     my $del = &db_query($delquery);
     
    } else {
     # under the limit - leave alone, but...
     # check if the file has a corresponding entry in the database
     my $recordexists = 0;
     my $checkfile_q = "SELECT count(*) FROM streamshifter_programs WHERE path_to_file = '$fullpathtofile'";
     my $sth4 = &db_query($checkfile_q);
     while (my $ref4 = $sth4->fetchrow_arrayref) {
      $recordexists = $$ref4[0];
     }
     if (!$recordexists) {
      
      # first we need to check that this file isn't being handled by another process (lame)
      # this leads to issues with double-entry in the database and itunes playlists
      # if it is, we need to just exit
      #my @check_progs = qw(lame wget mplayer scp rsync bladeenc);
      my $exit = 0;
      &log($program_id,"n/a","checking if file $file is being handled by another process...",$logfile,$logging);
      my $pid = &checkPid2($file);
      if ($pid) { 
       $exit = 1;
      }
      # as a second check, see if streamshifter itself is operating on this program
      &log($program_id,"n/a","checking if file $file (program id $program_id) is being handled by $script_name...",$logfile,$logging);
      my $search_tmp = 'r ' . $program_id . ' ';
      my ($pid2,$filesize2) = &checkPid($script_name,$search_tmp);
      if ($pid2) {
       $exit = 1;
      }
      
      if ($exit) {
       &log($program_id,"n/a","file $file is being handled by another process, exiting",$logfile,$logging);
      } else {
       
       # we collect what info we can from the file and add a record in the db for it
       my $write_secs = (stat($fullpathtofile))[9];
       my $name_timestamp = strftime( "%A, %B %d %I:%M:%S %p %Z",localtime($write_secs));
       my $full_timestamp = strftime( "%Y%m%d%H%M%S",localtime($write_secs));
       my $record_timestamp = strftime( "%Y-%m-%d %H:%M:%S",localtime($write_secs));
       my $thisyear = strftime( "%Y",localtime($write_secs));
       my ($time,$bitrate,$filesize,$finaltitle) = &getDuration($fullpathtofile);
       my $name = "";
       if (!$finaltitle) {
        my $tmpname = &basename($fullpathtofile);
        $tmpname =~ s/_/ /g;
        $tmpname =~ s/\d\d\d\d-\d\d-\d\d//g;
        $tmpname = &trim($tmpname);
        $tmpname =~ s/\b(\w)/\u$1/g;
        $finaltitle = $tmpname . " - " . $name_timestamp;
        if ($fullpathtofile =~ /\.[Mm][Pp]3$/) {
         &id3v2tag($fullpathtofile,$finaltitle,$thisyear,"","","","","");
        }
       } else {
        if ($finaltitle =~ /Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday/ig) {
         $name = $finaltitle;
        } else {
         $name = $finaltitle . " - " . $name_timestamp;
        }
       }
       $name = $dbh->quote($name);
       my $insertprog = "INSERT INTO streamshifter_programs SET
             schedule_id = '$program_id',
             title = $name,
             recording_date = '$record_timestamp',
             recording_length = '$time',
             bitrate = '$bitrate',
             filesize = '$filesize',
             path_to_file = '$fullpathtofile',
             info_url = '',
             timestamp = '$full_timestamp'
             ";
       #print "gonna insert $insertprog\n";
       my $insnew = &db_query($insertprog);
       if ($insnew) {
        &log($program_id,"n/a","inserted new program $name into recorded program listing",$logfile,$logging);
        if ($add_to_itunes eq 'Yes') {
         my $return = &iTunes($fullpathtofile,$add_to_itunes_playlists,$add_to_itunes,$itunes_bookmarkable);
        }
       }
      } # end if/else for file is being handled by another process.
     } # end NOT for record exists
    }
    $filecounter++;
   }
   
  } # end if for auto-create subdirs
  
  
 
 } # end while thru scheduled programs
 $sth->finish();
 
 # if we have any IDs, we need to delete
 if ($orphaned[0]) {
  my $ids = "'";
  $ids .= join("','",@orphaned);
  $ids .= "'";
  my $del = "DELETE FROM streamshifter_programs WHERE id IN($ids)";
  #print "Deleting program records with: $del\n";
  my $del_sth = &db_query($del);
  $del_sth->finish();
 }

} # end sub cleanPrograms

sub checkDay {
 # checks if the program is supposed to run TODAY
 
 my ($repeat_day,$day,$start_date,$date) = @_;
 
 my @weekend = qw(Saturday Sunday);
 my @weekdays = qw(Monday Tuesday Wednesday Thursday Friday);
 
 my $start_recording = 0;
 if ($repeat_day eq $day) {
  $start_recording = 1;
 } elsif (($repeat_day eq 'Once') && ($start_date eq $date)) {
  $start_recording = 1;
  #print "Starting one-time recording: $repeat_day $start_date eq $date\n";
 } elsif ($repeat_day eq 'Daily') {
  $start_recording = 1;
  #print "Starting daily recording: $repeat_day\n";
 } elsif ($repeat_day eq 'Weekdays') {
  my $found_weekday = 0;
  my $weekday_join = join(",",@weekdays);
  foreach my $weekday (@weekdays) {
   if ($day eq $weekday) {
    $found_weekday=1;
   }
  }
  if ($found_weekday) {
   $start_recording = 1;
   #print "Starting weekday recording: $repeat_day $day in $weekday_join\n";
  } else {
   $start_recording = 0;
   #print "NOT scheduled time: $repeat_day $day not in $weekday_join\n";
  }
 } elsif ($repeat_day eq 'Weekends') {
  my $found_weekend = 0;
  my $weekend_join = join(",",@weekend);
  foreach my $weekend (@weekend) {
   if ($day eq $weekend) {
    $found_weekend=1;
   }
  }
  if ($found_weekend) {
   $start_recording = 1;
   #print "Starting weekend recording: $repeat_day $day in $weekend_join\n";
  } else {
   $start_recording = 0;
   #print "NOT scheduled time: $repeat_day $day not not in $weekend_join\n";
  }
 } else {
  $start_recording = 0;
 }
 
 #print "start_recording: $start_recording\n";
 return $start_recording;

} # end sub checkDay

sub dateAnchors {
 # if replaces mysql-specific date anchors in strings with date info

 my ($string,$adjust,$interval,$interval_type) = @_;
 
 if (!$interval_type) {
  $interval_type = "HOUR";
 }
  
 # only do this if there are date specifiers in the string - we have to make sure they are url encodings (which can use similar specifiers, but use 2 characters instead of 1)
 if ( ($string =~ /%[abcDdefHhIijklMmprSsTUuVvWwXxYyZ]/) && (not($string =~ /%[abcdef][abcdef0-9]/)) ) {
  
  my $string2 = $string;
  my $timezone = strftime( "%Z",localtime(time));
  my $date_modifier = "DATE_ADD";
  if ($interval eq 'Past') {
   $date_modifier = "DATE_SUB";
  }
  if (!$adjust) {
   $adjust = 0;
  }
 
  while ($string =~ /(%[abcDdefHhIijklMmprSsTUuVvWwXxYy])/g) {
   
   my $dateq = "SELECT DATE_FORMAT($date_modifier(NOW(), INTERVAL $adjust $interval_type),'$1')";
   # this is kludgey, but if $adjust != 1 && $interval != 'Future' && $interval_type != MINUTE 
   #then we need to be sure that we adjust MYSQL's NOW() forward 1 minute since we start the process 1 minute before programs begine recording
   if (($adjust ne '1') && ($interval ne 'Future') && ($interval_type ne 'MINUTE')) {
    $dateq = "SELECT DATE_FORMAT($date_modifier(DATE_ADD(NOW(),INTERVAL 1 MINUTE), INTERVAL $adjust $interval_type),'$1')";
   }
   
   #print "$dateq\n";
   my $sthq = &db_query($dateq);
   my $replace = "";
   while (my $ref = $sthq->fetchrow_arrayref) {
    $replace = $$ref[0];
   }
   $sthq->finish();
   $string2 =~ s/$1/$replace/g;
  }
  
  # timezone is a little different.  %Z doesn't mean anything to mysql
  if ($string =~ /(%Z)/) {
   $string2 =~ s/$1/$timezone/g;
  }
  
  #print "found anchors and adjsuted string $string -> $string2\n";
  $string = $string2;
 }
 
 return($string);

} # end sub dateAnchors

#################################################################################################
###################    SUBROUTINES HAVING TO DO TESTING OF THE URL/STREAM   #####################
#################################################################################################

sub urlGetMethod {
 # creates the command we will use to save our program

 my ($url,$temp_filepath,$mplayer,$nice,$wget,$ext,$path_to_rawdata) = @_;
 my $cmd = "";
 my $get_prog = "";
 my $gonna_wget = 0;
 
 # actually determine the file type
 my ($filetype,$khz,$asfflag,$filetype_success,$failreason) = &determineFileType($url,$wget,$mplayer,$nice,$path_to_rawdata);
 
 # set mplayer cache settings if NOT an asf
 my $mplayer_cache = "";
 if (!$asfflag) {
  $mplayer_cache = "-cache 8192 -cache-min 4 ";
 }
 
 if ($filetype_success == 1) {
  if ($filetype eq "File") {
   # if it's not a stream, we need to wget it.
   $url =~ /\.([OoMm][GgPp][Gg3])/; 
   my $ext_tmp = lc($1);
   $temp_filepath =~ s/$ext/$ext_tmp/g;
   #print "EXT: $ext EXT_TMP: $ext_tmp TEMP_FILEPATH: $temp_filepath\n";;
   $cmd = "${nice}$wget '$url' -nv --output-document=$temp_filepath";
   $gonna_wget = 1;
   $get_prog = "wget";
   $ext = $ext_tmp;
  } elsif  ($filetype eq 'Stream') {
   #$cmd = "$mplayer '$url' -dumpstream -dumpfile $temp_filepath";
   $cmd = "${nice}$mplayer -really-quiet -bandwidth 5000000 ${mplayer_cache}-vc dummy -vo null -af volume=0,channels=2,format=s16le -srate 44100 -ao pcm:waveheader:file=$temp_filepath '$url'";
   if ($logging) { $cmd .= " >> $logfile 2>&1"; }
   $get_prog = "mplayer";
  } else {
   #$cmd = "$mplayer -playlist '$url' -dumpstream -dumpfile $temp_filepath";
   $cmd = "${nice}$mplayer -really-quiet -bandwidth 5000000 ${mplayer_cache}-vc dummy -vo null -af volume=0,channels=2,format=s16le -srate 44100 -ao pcm:waveheader:file=$temp_filepath -playlist '$url'";
   if ($logging) { $cmd .= " >> $logfile 2>&1"; }
   $get_prog = "mplayer";
  }
 }
 return ($filetype_success,$khz,$cmd,$get_prog,$temp_filepath,$ext,$failreason);
 
} # end sub urlGetMethod

sub determineFileType {
 # determines the type of url this is (file, stream, playlist).  related to urlGetMethod

 my ($url,$wget,$mplayer,$nice,$datadir)= @_;
 my $filetype = "";
 my $success = 0;
 my $sleep = 20; # DO NOT CHANGE THIS!  amount of time to get response from url - this sleep happens twice (once for wget test and once for mplayer test).  this entire process (sleep+sleep2) should take 46 seconds exactly. 
 my $sleep2 = 6; # DO NOT CHANGE THIS!  Since sleep*2+sleep2 = 46, mplayer should start trying to make the connection 14 seconds before the program's scheduled time
 my $failreason = "";
 my $failedflag = 0;
 my $khz = 0;
 my $asfflag = 0;
 
 # output log file and test sound file
 my $rand = &generate_random_string(12);
 my $log = $datadir . $rand . '.txt';
 my $test_sound = $datadir . $rand . '.wav';
 
 # only do this if the url begins with http
 if ($url =~ /^[Hh][Tt][Tt][Pp]:\/\//) {
 
  # usually it's a playlist, but we need to test
  $filetype = 'Playlist';
  
  # for the wget process
  my $wget_timeout = &round(($sleep-1)/2); # account for 2 tries
  
  my $cmd = "${nice}$wget --timeout=$wget_timeout -t 2 -o $log --output-document=/dev/null '$url'";
  &log("n/a","n/a","testing $url with $cmd",$logfile,$logging);
  my $wget_pid = fork();
    
  # child process
  if ($wget_pid == 0) {
   
   exec("$cmd");
   
  } elsif ($wget_pid > 0) {
   
   # wait, then grab the pid of the wget process and kill it
   sleep $sleep;
   my $killsuccess = &handlePid($wget,$url,$wget_pid,$log);
   if (!$killsuccess) {
    &log("n/a","n/a","Could not kill the $wget process! pid: $wget_pid",$logfile,$logging);
   }
   
  }
  
  # lets open our file and examine it
  open(FILE,"$log");
  my @lines = ;
  close(FILE);
  my $lengthfield = "";
  foreach my $line (@lines) {
   &log("n/a","n/a",$line,$logfile,$logging);
   if ($line =~ /^Resolving/) {
    if ($line =~ /failed/) {
     $failedflag = 1;
     $failreason .= "Failed resolving URL with this command:\n\n$cmd ";
    } 
   }
   elsif ($line =~ /[Bb]ad\sport/) {
    $failedflag = 1;
    $failreason .= "Bad port number.  Used this command:\n\n$cmd ";
   }
   elsif ($line =~ /404\s[Nn]ot\s[Ff]ound/) {
    $failedflag = 1;
    $failreason .= "Got 404 Not Found error - check URL for typos.  Used this command:\n\n$cmd ";
   }
   elsif ($line =~ /[Oo]peration\stimed\sout/) {
    $failedflag = 1;
    $failreason .= "Got 'Operation timed out' error with:\n\n$cmd ";
   }
   elsif ($line =~ /^Length/) {
    $line =~ /^Length: (.*)$/;
    $lengthfield = $1;
    if ($lengthfield =~ /x-ms-asf/) {
     $asfflag = 1;
    }
   } else {
    # nothing at this time
   }
  } # end foreach

  if ($failedflag == 0) {
   $success = 1;
   # what kid of url is it?
   if ($lengthfield =~ /unspecified/) {
    if ( ($lengthfield =~ /mpegurl/) || ($lengthfield =~ /scpls/) || ($lengthfield =~ /realaudio/) ) {
     $filetype="Playlist";
    } else {
     $filetype = 'Stream';
    }
   } else {
    # if it's over 150kb, let's treat it as a file
    $lengthfield =~ /^\s?(\d+)\s/;
    my $bytes = $1;
    if ($bytes >= 150000) {
     $filetype = 'File';
    } else {
     if ($asfflag==1) {
      #$filetype = 'Stream';
      $filetype = 'Stream';
     } else {
      $filetype = 'Playlist';
     }
    }
   }
  } else {
  
   $success = 0;
   
  }# end if for failedflag

 } else {
  
  # url doesn't begin with http, we assume it's a stream but we'll test next
  $filetype = 'Stream';
  $success = 1;
  &log("n/a","n/a","Appears to be a stream so we bypassed the wget check, sleeping $sleep until we test URL with mplayer.  URL: $url",$logfile,$logging);
  sleep $sleep;  
 }
 
 if ($success == 1) {
  &log("n/a","n/a","$url appears to be a $filetype",$logfile,$logging);
 }
 
 ### TEST THE STREAM IF IT'S NOT A FILE
 if (($filetype ne 'File') && ($success == 1)) {
 
  my $playlistmarker = "";
  if ($filetype eq 'Playlist') {
   $playlistmarker = ' -playlist';
  }
  
  # for the mplayer process
  #my $cmd = "$mplayer$playlistmarker '$url' -dumpstream -dumpfile $test_sound > $log 2>&1";
  my $cmd = "${nice}$mplayer -bandwidth 5000000 -cache 32 -cache-min 0 -nocache -vc dummy -vo null -af volume=0,channels=2 -ao pcm:waveheader:file=$test_sound $playlistmarker '$url' > $log 2>&1";
  &log("n/a","n/a","testing stream $url with $cmd",$logfile,$logging);
  my $mplayer_pid = fork();
    
  # child process
  if ($mplayer_pid == 0) {
  
   exec("$cmd");
   
  } elsif ($mplayer_pid > 0) {
   
   # wait, then grab the pid of the wget process and kill it
   sleep $sleep;
   my $killsuccess = &handlePid($mplayer,$url,$mplayer_pid,$test_sound);
   if (!$killsuccess) {
    &log("n/a","n/a","Could not kill the $mplayer process! pid: $mplayer_pid",$logfile,$logging);
   }
  }
  
  # lets open our logfile file and examine it
  open(FILE,"$log");
  my @lines = ;
  close(FILE);
  my $successflag = 0;
  foreach my $line (@lines) {
   if (($line =~ /^[Cc]onnected/) || ($line =~ /^ICY\sInfo/) || ($line =~ /^[Ss]tream\s[Nn]ot\s[Ss]eekable/) || ($line =~ /Audio\sstream\sfound/) || ($line =~ /Starting\splayback/)) {
    $successflag = 1;
   }
   if ($line =~ /^[Ee]rror/) {
    $line =~ s/\n//g;
    $failreason .= "${line}.\n";
   }
   if ($line =~ /^[Uu]nable/) {
    $line =~ s/\n//g;
    $failreason .= "${line}.\n";
   }
   if ($line =~ /^[Aa][Uu][Dd][Ii][Oo]: (\d+) Hz,/) {
    $khz = $1;
   }
   #print "LINE: $line";
  } # end foreach
  
  if ($successflag == 1) {
   # before assuming we have success, we need to see if there's an audiofile
   if (not(-e $test_sound)) {
    $success = 0;
    $failreason .= "\nmplayer could not create a test data file with this command:\n\n$cmd ";
   } else {
    $success = 1;
    #print "URL is valid and working...\n";
   }
  } else {
   $success = 0;
   $failreason .= "\nmplayer could not connect to server with this command:\n\n$cmd ";
  }
    
 } else {
  
  # just sleep so that we can stay on schedule
  &log("n/a","n/a","Appears to be a file so we bypassed the mplayer check, sleeping $sleep until we start getting the program.  URL: $url",$logfile,$logging);
  sleep $sleep;
  
 }# end if/else for NOT a file (stream testing)  
 
 # remove our temp file
 if (-e $log) {
  unlink($log);
 }
 # remove our temp file
 if (-e $test_sound) {
  unlink($test_sound);
 }

 if ($success == 1) {
  &log("n/a","n/a","sleeping $sleep2 more seconds until our program begins...",$logfile,$logging);
  sleep $sleep2;
 }


 return ($filetype,$khz,$asfflag,$success,$failreason);
 
} # end sub determineFileType

#################################################################################################
################### SUBROUTINES HAVING TO DO WITH RECORDING/SAVING/ENCODING #####################
#################################################################################################

sub getProgramInfo {
 # loads up our program's information (url, duration, title, etc)

 my ($spec_id,$action,$path_to_library,$path_to_desktop,$rec_url,$rec_minutes,$id3_timestamp_format,$dbh) = @_;
 my ($id,$adjust_info_url_interval,$path,$temp_file,$final_file,
     $temp_filepath,$file_filepath,$final_filepath,
     $ext,$user_id,$title,$author,$album,$composer,$genre,$url,$adjust_url,
     $adjust_url_interval,$info_url,$adjust_info_url,
     $duration_min,$duration_sec,$output_bitrate,$adjust_url,$add_to_itunes_playlists,$itunes_bookmarkable,
     $username,$contact_name,$contact_email,$email_alerts,$id3_timestamp);
 
 my $sql = "SELECT * FROM streamshifter_schedule WHERE id = '$spec_id' LIMIT 1";
 my $sth = &db_query($sql);
 while (my $ref = $sth->fetchrow_hashref) {

  # load the user associated with this program
  # if we don't hyave a userID, w eneed to die.
  if (!$ref->{'user_id'}) {
   my $message = "THERE IS NO USER ASSOCIATED WITH PROGRAM ID $spec_id!";
   &log($spec_id,$title,$message,$logfile,$logging);
   &fail($spec_id,"","$message");
   exit 1;
  }
  ($username,$contact_name,$contact_email,$email_alerts) = &loadUser($ref->{'user_id'});
  &log($spec_id,$title,"Loaded user: $username,$contact_name,$contact_email,$email_alerts",$logfile,$logging);
  
  # print timestamp
  my $ts_start = strftime( "%Y%m%d%H%M%S",localtime(time));
  &log($spec_id,$title,"starting recording of $ref->{'title'} at $ts_start",$logfile,$logging);
 
  # make our paths
  ($path,$temp_file,$final_file,$temp_filepath,$file_filepath,$final_filepath,$ext) = &getPaths($ref->{'title'},$action,$path_to_library,$path_to_desktop,$ref->{'save_to_library'});
  
  # get other data we need
  $id = $ref->{'id'};
  $user_id = $ref->{'user_id'};
  $title = &stripChars($ref->{'title'});
  $author = &stripChars($ref->{'author'});
  $album = &stripChars($ref->{'album_or_episode'});
  if (!$album) { $album = &stripDateSpec($title); }
  $composer = &stripChars($ref->{'composer_or_network'});
  $genre = &stripChars($ref->{'genre'});
  $url = &stripChars($ref->{'url'});
  if (length($rec_url)>0) { $url = $rec_url; }
  $adjust_url = $ref->{'adjust_url'};
  $adjust_url_interval = $ref->{'adjust_url_interval'};
  $adjust_info_url_interval = $ref->{'adjust_info_url_interval'};
  $info_url = &stripChars($ref->{'info_url'});
  $adjust_info_url = $ref->{'adjust_info_url'};
  $duration_min = $ref->{'duration_minutes'};
  if ($rec_minutes > 0) { $duration_min = $rec_minutes; }
  $duration_sec = ($duration_min*60); 
  $output_bitrate = $ref->{'output_bitrate'};
  $add_to_itunes_playlists = $ref->{'add_to_itunes_playlists'};
  $itunes_bookmarkable = $ref->{'itunes_bookmarkable'};
  $adjust_url = $ref->{'adjust_url'};
  
  # if the url has a date in it, replace the specifiers (MYSQL-compliant)
  if (length($id3_timestamp_format)>0) {
   $id3_timestamp = &dateAnchors($id3_timestamp_format,1,"Future","MINUTE");
   $title .= " - " . $id3_timestamp;
  } else {
   $title = &dateAnchors($title,1,"Future","MINUTE") ;
  }
  $author = &dateAnchors($author,1,"Future","MINUTE") ;
  $album = &dateAnchors($album,1,"Future","MINUTE");
  $composer = &dateAnchors($composer,1,"Future","MINUTE");
  $url = &dateAnchors($url,$adjust_url,$adjust_url_interval,"HOUR");
  $info_url = &dateAnchors($info_url,$adjust_info_url,$adjust_info_url_interval,"HOUR");
  
 } # end while thru db results
 $sth->finish();
 
 return ($path,$temp_file,$final_file,$temp_filepath,$file_filepath,$final_filepath,$ext,$user_id,$title,$author,$album,$composer,$genre,$url,$adjust_url,$adjust_url_interval,$info_url,$adjust_info_url,$duration_min,$duration_sec,$output_bitrate,$add_to_itunes_playlists,$itunes_bookmarkable,$username,$contact_name,$contact_email,$email_alerts);

} # end sub getProgramInfo

sub loadUser {
 # loads up the user associated with this program
 
 my $user_id = shift;
 
 my ($username,$contact_name,$contact_email,$email_alerts) = "";
 my $user_q = "SELECT * FROM streamshifter_users WHERE id = '$user_id' LIMIT 1";
 my $sth_user = &db_query($user_q);
 while (my $ref_user = $sth_user->fetchrow_hashref) {
  $username = $ref_user->{'username'};
  $contact_name = $ref_user->{'name'};
  $contact_email = $ref_user->{'email'};
  $email_alerts = $ref_user->{'email_alerts'};
 }
 $sth_user->finish();
 
 return ($username,$contact_name,$contact_email,$email_alerts);
 
} # end sub loadUser

sub getPaths {
 # creates the paths we will use for our various files

 my ($title,$action,$path_to_library,$path_to_desktop,$save_to_library) = @_;

 my $path = "";
 if ($save_to_library eq 'Yes') { 
  my $tempname = lc(&replaceSpace(&stripDateSpec($title)));
  my $dir = $path_to_library . $tempname . '/';
  my $checkdir = &checkLibDir($dir);
  if ($checkdir) {
   $path = $dir;
  } else {
   $path = $path_to_desktop;
  }
 } else {
  $path = $path_to_desktop;
 }# end if/else for save_to_library = Yes
 
 # if it's a test, we just set path to default_loc
 if ($action eq 'test') {
  $path = $path_to_desktop;
 }
 #print "will save file to $path\n";
 
 # create the name of our outputs file and make a full path to them
 # temporary file extension
 my $ext = 'wav';
 my $temp_file = &replaceSpace(&stripDateSpec($title)) . "_$ts2.$ext";
 my $final_file = &replaceSpace(&stripDateSpec($title)) . "_$ts2.mp3";
 my $temp_filepath = $path_to_rawdata . $temp_file;
 my $file_filepath = $path_to_rawdata . $final_file;
 my $final_filepath = $path . $final_file;
 
 return ($path,$temp_file,$final_file,$temp_filepath,$file_filepath,$final_filepath,$ext);
 
} # end sub getPaths

sub mplayerProgram {
 # this is the real meat - the mplayer rip and encode

 my ($pid,$path,$spec_id,$title,$khz,$mplayer,$sox,$normalize,$lame,$nice,$url,$ext,$temp_filepath,$file_filepath,
  $final_file,$final_filepath,$email_alerts,$sendmail,$contact_email,
  $contact_name,$duration_sec,$duration_min,$title,$author,$composer,$output_bitrate,
  $album,$year,$info_url,$genre,$date) = @_;
 
 # sleep x seconds to allow for a connection
 &log($spec_id,$title,"sleeping 14 seconds to allow for mplayer to connect",$logfile,$logging);
 sleep 14; # do not change this value
 
 # this is an mplayer stream
 &log($spec_id,$title,"sleeping $duration_sec seconds while program $title records",$logfile,$logging);

 # split our time into tenths, then loop thru our duration 10 times and be sure mplayer is running
 # this is so that if mplayer fails, our script doesn't keep running the entire duration of the program.
 my $time_split = ($duration_sec/10);
 my $filesize_prev = 0;
 for (my $i=0;$i<10;$i++) {
  
  if ($i==0){
   sleep $time_split;
  }
  my ($result,$filesize) = &checkPid($mplayer,$temp_filepath);
  if ($result) {
   #print "sub-sleeping $time_split\n";
   if ($i>0){
    my $message = "filesize at this point: $filesize bytes. Last time it was $filesize_prev";
    &log($spec_id,$title,$message,$logfile,$logging);
    sleep $time_split;
   }
  } else {
   my $message = "mplayer was found to not be running when it was supposed to be, cannot record $temp_filepath";
   &log($spec_id,$title,$message,$logfile,$logging);
   if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
   &fail($spec_id,$temp_filepath,$message);
   exit 1;
  }
  $filesize_prev = $filesize;
 } # end for loop thru time
 
 # kill mplayer
 #print "stopping mplayer process...\n";
 my $mpkill = &handlePid($mplayer,$url,$pid,$temp_filepath);
 if (!$mpkill) {
  &log($spec_id,$title,"Could not kill the $mplayer process! pid: $pid",$logfile,$logging);
 }
 
 # make sure we have a file at this point.  If not, we couldn't create the stream
 if (not(-e $temp_filepath)) {
  my $message = "No raw recording file was found: $temp_filepath";
  &log($spec_id,$title,$message,$logfile,$logging);
  if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
  &fail($spec_id,$temp_filepath,"$message");
  exit 1;
 }
 
 # encode as MP3
 &log($spec_id,$title,"converting $temp_filepath to $final_file",$logfile,$logging);
 # check encoder
 my $encode_string = "";
 
 # ID3 tags
 my $id3 = " --add-id3v2 --ignore-tag-errors --tt \"$title\" --ta \"$author $composer\" --tl \"$album\" --ty \"$year\" --tc \"$info_url\" --tg \"$genre\"";
 
 # do the encode
 # lame, normalize and sox are pretty intense, so let's check if it's already encoding another file and if so, wait
 my $process_check1 = &process_check($lame,'vbr-new',$spec_id,$temp_filepath,$action,$email_alerts,$sendmail,$contact_email,$contact_name);
 my $process_check2 = &process_check($sox,'resample',$spec_id,$temp_filepath,$action,$email_alerts,$sendmail,$contact_email,$contact_name);
 my $process_check3 = &process_check($normalize,'wav',$spec_id,$temp_filepath,$action,$email_alerts,$sendmail,$contact_email,$contact_name);
 
 if (($process_check1) && ($process_check2) && ($process_check3)) {
  # check to see if the file needs to be resampled
  ($temp_filepath,$khz) = &resample($temp_filepath,$khz,$sox,$nice,$spec_id,$title,$logfile,$logging);
  # normalize the wav file
  &normalize($temp_filepath,$normalize,$nice,$spec_id,$title,$logfile,$logging);
  $encode_string = "${nice}$lame -V0 -h -b $output_bitrate --quiet --vbr-new$id3 $temp_filepath $file_filepath"; 
 }   
 
 if ($logging) { $encode_string .= " >> $logfile 2>&1"; }
 &log($spec_id,$title,"encoding with command: $encode_string",$logfile,$logging);
 system("$encode_string");

} # end sub mplayerProgram

sub resample {
 # resamples the wav to something conventional
 
 my ($file,$khz,$sox,$nice,$spec_id,$title,$logfile,$logging) = @_;
 &log($spec_id,$title,"checking if we need to RESAMPLE with sox.  our khz is currently: $khz",$logfile,$logging);
 
 # check if the current khz is valid
 my @valid = qw(32000 44100 48000);
 my $hasvalidkhz = 0;
 foreach my $hz (@valid) {
  if ($hz==$khz) {
   $hasvalidkhz = 1;
  }
 }
 
 # change the khz if not valid
 if (!$hasvalidkhz) {
  
  my $newkhz;
  if ($khz < 32000) { $newkhz = 44100; }
  if (($khz > 32000) && ($khz < 44100)) { $newkhz = 44100; }
  if (($khz > 44100) && ($khz < 48000)) { $newkhz = 44100; }
  if ($khz > 48000)  { $newkhz = 44100; }
  
  # mv the file to a temp file
  $file =~ /^(.*?)\.wav$/;
  my $temp = $1 . '.tmp.wav';
  system("/bin/mv $file $temp");
  
  my $soxcmd = "${nice}$sox --ignore-length $temp -r $newkhz $file";
  if ($logging) { $soxcmd .= " >> $logfile 2>&1"; }
  &log($spec_id,$title,"resampling $file from $khz to $newkhz with $soxcmd",$logfile,$logging);
  system("$soxcmd");
  $khz = $newkhz;
  
  # delete our temp file
  if (-e $temp) {
   if(not(unlink($temp))) {
    &log($spec_id,$title,"could not delete $temp",$logfile,$logging);
   }
  }
  
 } # end if for validkhz
 
 return ($file,$khz);

} # end sub resample

sub normalize {
 # normalizes the wav to default normalize setting
 
 my ($file,$normalize,$nice,$spec_id,$title,$logfile,$logging) = @_;
 my $normcmd = "${nice}$normalize $file";
 if ($logging) { $normcmd .= " >> $logfile 2>&1"; }
 &log($spec_id,$title,"normalizing $file f with $normcmd",$logfile,$logging);
 system("$normcmd");

} # end sub normalize

sub wgetProgram {
 # if the url is a file, we use wget instead of mplayer
 
 my ($pid,$path,$spec_id,$title,$wget,$ext,$temp_filepath,
  $file_filepath,$final_filepath,$email_alerts,
  $sendmail,$contact_email,$contact_name,$duration_sec,
  $duration_min) = @_;
  
 # we just retrieve the file.  it's done when wget is no longer running. but we do want to kill wget after our $duration_sec if the file turns out to be a stream
 my $time_split = ($duration_sec/$duration_min);
 my $time_tracker = 0;
 for (my $i=0;$i<$duration_min;$i++) {
  my ($result,$filesize) = &checkPid($wget,$temp_filepath);
  if ($result) {
   &log($spec_id,$title,"sleeping $time_split filesize: $filesize",$logfile,$logging);
   sleep $time_split;
   $time_tracker = $time_tracker+$time_split;
   if ($time_tracker >= $duration_sec) {
    # kill wget
    &log($spec_id,$title,"stopping wget process: $result",$logfile,$logging);
    #kill("15",$result);
    my $kill = `/bin/kill -15 $result`;
   }
  } else {
   last;
  }
 } # end for loop thru time
 
 if (-e $temp_filepath) {
  my $message = "wget finished getting $temp_filepath";
  &log($spec_id,$title,$message,$logfile,$logging);
  # need to update our paths
  $file_filepath = $temp_filepath;
  $final_filepath = $path . &replaceSpace(&stripDateSpec($title)) . "_$ts2." . $ext;
 } else {
  my $message = "wget failed to save recording to: $temp_filepath";
  &log($spec_id,$title,$message,$logfile,$logging);
  if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
  &fail($spec_id,$temp_filepath,"$message");
  exit 1;
 }

} # end sub wgetProgram

sub tagFile {
 # tags our mp3 or ogg with meta data
 
 my ($title,$author,$composer,$year,$album,$file_filepath,$info_url,$genre,$use_mp3tag,$vorbiscomment,$my_encoder) = @_;
 
 # tag properly if it's an mp3 and we have the module
 my $tagtitle = "$title";
 my $tagauthor = "$author $composer";
 &id3v2tag($file_filepath,$tagtitle,$year,$tagauthor,$album,$info_url,$genre,"lame");
 
 return 1;

} # end sub tagFile

sub id3v2tag {
 # adds id3v2 tag to mp3s

 my ($file,$title,$year,$author,$album,$info_url,$genre,$encoder) = @_;

 # my $version = "$MP3::Tag::VERSION";
 eval {
     my $mp3 = MP3::Tag->new($file);
     $mp3->get_tags();
     $mp3->new_tag("ID3v2");
     $mp3->{ID3v2}->remove_tag();
     $mp3->{ID3v2}->add_frame("TALB", "$album");
     $mp3->{ID3v2}->add_frame("TIT2", "$title");
     $mp3->{ID3v2}->add_frame("TPE1", "$author");
     $mp3->{ID3v2}->add_frame("TCON", "$genre");
     $mp3->{ID3v2}->add_frame("TSSE", "$encoder");
     $mp3->{ID3v2}->add_frame("TYER", "$year");
	 #$mp3->{ID3v2}->add_frame('COMM', "$info_url", 'eng', "$info_url");
	 $mp3->{ID3v2}->frame_select_by_descr("COMM(eng,#0)[]","$info_url");
     #$mp3->{ID3v2}->add_frame("TIT3", "$info_url");
     #$mp3->{ID3v2}->add_frame("TRSN", "$info_url");
     #$mp3->{ID3v2}->add_frame('TXXX', "$info_url", 'eng', "$info_url");
     #$mp3->{ID3v2}->add_frame("WORS", "$info_url");
     #$mp3->{ID3v2}->add_frame("WXXX", "$info_url", 'eng', "$info_url");
     $mp3->{ID3v2}->write_tag;
     $mp3->close();
 }; warn $@ if $@;
return 1;

} # end sub id3v2tag

sub cleanAndFinish {
 # cleans up our recording programs
 
 my ($spec_id,$title,$file_filepath,$final_filepath,$get_prog,$temp_filepath,$email_alerts,$sendmail,$contact_email,$contact_name) = @_;
 if (-e $file_filepath) {
  if ($get_prog ne 'wget') {
   if (unlink($temp_filepath)) {
    &log($spec_id,$title,"removed $temp_filepath",$logfile,$logging);
   } else {
    my $message = "Could not remove $temp_filepath\n";
    if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
    &fail($spec_id,$temp_filepath,"$message");
    &log($spec_id,$title,$message,$logfile,$logging);
   }
  }
  system("/bin/mv $file_filepath $final_filepath");
 } else {
  if (-e $temp_filepath) {
   unlink($temp_filepath);
  } 
  my $message = "final output mp3 file ($file_filepath) was not encoded - problem with encoder or sox";
  &log($spec_id,$title,$message,$logfile,$logging);
  if ($email_alerts = 'Yes') { &emailAlert($sendmail,"Problem Recording \"$title\"",$message,"",$contact_email,$contact_name); }
  &fail($spec_id,$temp_filepath,"$message");
  exit 1;
 }

} # end sub cleanAndFinish

sub getDuration {
 # gets the duration of the file in question 

 my $filepath = shift;
 
 my $seconds = 0;
 my $bitrate = 0;
 my $finaltitle = "";
 
  # it's an mp3
  use MP3::Info;
  my $info = get_mp3info($filepath);
  my $tag = get_mp3tag($filepath);
  $seconds = $info->{SECS};
  $bitrate = $info->{BITRATE};
  $finaltitle = $tag->{TITLE};
  #print "TITLE IS: $info->{TITLE} $info->{ALBUM} $info->{ARTIST}\n";

 my $time = &convertSeconds($seconds);
 
 # get the file size
 my $fstemp = -s $filepath;
 my $fstemp2 = ($fstemp/1024);
 my $filesize = ($fstemp2/1024);
 $filesize = sprintf("%.1f", $filesize);
 
 return ($time,$bitrate,$filesize,$finaltitle);
 
} # end sub getduration

sub success {
 
 my ($id,$filepath,$title,$info_url,$htmldir,$contact_email,$contact_name,$email_alerts,$action) = @_;
 my $timestamp = strftime( "%Y%m%d%H%M%S",localtime(time));
 
 # get the duration
 my ($duration,$bitrate,$filesize,$finaltitle) = &getDuration($filepath);
 &log($id,$title,"recording length: $duration",$logfile,$logging);
 
 my $message = "New episode of $title successfully recorded.  Check it out!\n\nFile: $filepath\n\nDuration: $duration";
 &log($id,$title,$message,$logfile,$logging);
 if (($email_alerts = 'Yes') && ($action ne 'test')) { &emailAlert($sendmail,"New Episode: \"$title\"",$message,"",$contact_email,$contact_name); }
 $message = $dbh->quote($message);
 
 # update the scheduled program record with recent run info
 my $successq = "UPDATE streamshifter_schedule SET last_run = '$timestamp', last_run_result = 'Success', last_run_comments = $message WHERE id = '$id'";
 my $success = &db_query($successq);
 
 # add this row to the programs table
 $title = $dbh->quote($title);
 
 # first check to see that it's not already there
 my $count = 0;
 my $check_sql = "SELECT count(*) FROM streamshifter_programs WHERE path_to_file = '$filepath'";
 my $sthc = &db_query($check_sql);
 while (my $refc = $sthc->fetchrow_arrayref) {
  $count = $$refc[0];
 }
 if ($count == 0) {
  my $insertq = "INSERT into streamshifter_programs SET schedule_id = '$id', title = $title, recording_date = '$timestamp', info_url = '$info_url', recording_length = '$duration', bitrate = '$bitrate', filesize = '$filesize', path_to_file = '$filepath'";
  my $insert = &db_query($insertq);
 }

} # end sub success

sub fail {
 
 my ($id,$temp_filepath,$comments) = @_;
 my $timestamp = strftime( "%Y%m%d%H%M%S",localtime(time));
 $comments = $dbh->quote($comments);
 my $failq = "UPDATE streamshifter_schedule SET last_run = '$timestamp', last_run_result = 'Fail', last_run_comments = $comments WHERE id = '$id'";
 my $fails = &db_query($failq);
 if (-e $temp_filepath) {
  unlink($temp_filepath);
 }
 &log($id,"n/a","Rip failed, exiting: $comments",$logfile,$logging);
 
} # end sub fail

#################################################################################################
############  SUBROUTINES HAVING TO DO WITH CHECKING/KILLING RUNNING PROCESSES  #################
#################################################################################################

sub handlePid {
 # kills the PID that matches a certain criteria

 my ($grep1,$grep2,$possible_pid,$grep3) = @_;
 my @running = ();
 my $killflag = 0;
 my $pidgetter = "";
 if ($grep3) {
  $pidgetter = "/bin/ps ax -o pid,command | /usr/bin/grep '$grep1' | /usr/bin/grep '$grep2' | /usr/bin/grep '$grep3' | /usr/bin/grep -v 'grep' | /usr/bin/grep -v 'sh -c'";
 } else {
  $pidgetter = "/bin/ps ax -o pid,command | /usr/bin/grep '$grep1' | /usr/bin/grep '$grep2' | /usr/bin/grep -v 'grep' | /usr/bin/grep -v 'sh -c'";
 }
 
 &log("n/a","n/a","getting PID of running program with $pidgetter",$logfile,$logging);
 my @raw_pids = `$pidgetter`;
 #print "RAW\n";
 #print @raw_pids;
 chomp(@raw_pids);
 
 # if there are no raw_pids (shoudln't happen), throw in the possible pid from perl
 if (!$raw_pids[0]) {
  push(@raw_pids,$possible_pid);
 }
 
 foreach my $raw_pid (@raw_pids) {
  $raw_pid =~ s/^\s+//;
  $raw_pid =~ /^(\d+)\s/;
  $raw_pid = $1;
  my $pid = stripNonNumber($raw_pid);
  #print "PIDS: pid: $pid raw: $raw_pid\n";
  if ($pid) {
  my $exists = kill("0",$pid);
  if ($exists) {
   &log("n/a","n/a","found pid of running program: $pid ($grep1,$grep2,$grep3)",$logfile,$logging);
   push(@running,$pid);
  }
  }
 } # end foreach
 
 if ($running[0]) {
  # loop thru and kill the pids gently, then forcibly if necessary
  foreach my $pid (@running) {

   #my $killresult = kill("15",$pid);
   my $killresult = `/bin/kill -15 $pid`;
   
   &log("n/a","n/a","tried kill with: /bin/kill -15 $pid , got result: $killresult",$logfile,$logging);

   # check if it's still running'
   my ($killcheck1,$del) = &checkPid($grep1,$grep2);
   if ($killcheck1) {
   	# things are being killed properly, I just haven't had time to figure out why
   	# these incorrect messages are showing up in the log.  
    &log("n/a","n/a","Could not stop $pid with signal 15, trying with signal 9",$logfile,$logging);
	     my $killresult2 = `/bin/kill -9 $pid`;
	     &log("n/a","n/a","tried kill with: /bin/kill -9 $pid , got result: $killresult2",$logfile,$logging);
	     my ($killcheck2,$del) = &checkPid($grep1,$grep2);
	     if ($killcheck2) {
	      &log("n/a","n/a","still couldn't kill $pid, even with signal 9",$logfile,$logging);
	     } else {
	      $killflag = 1;
	      &log("n/a","n/a","$pid killed with signal 9",$logfile,$logging); 
	     }
   } else {
    $killflag = 1;
    &log("n/a","n/a","Stopped $pid with signal 15",$logfile,$logging);
   }


  } # end foreach thru pids
  if ($killflag == 0) {
   return 0;
  } else {
   return 1;  
  }
  
 } else {
  # no pids running, nothing to do.
  return 1;
 }
  
} # end handlePid

sub checkPid {
 # gets the PID of the process that matches a certain criteria

 my ($program,$grep) = @_;
 my @running = ();
 my $return_pid = "";
 
 # if the $grep var is a media file, let's get the filesize
 my $filesize = 0;
 if (($grep =~ /[MmOoWw][PpGgAa][3GgVv]$/) || ($grep =~ /mpfile$/)) {
  $filesize = (-s $grep);
 }
 my $pidgetter = "/bin/ps ax -o pid,command | /usr/bin/grep '$program' | /usr/bin/grep '$grep' | /usr/bin/grep -v 'grep' | /usr/bin/grep -v 'sh -c'";
 #print "checking for running program with $pidgetter\n";
 &log("n/a","n/a","checking for running program with $pidgetter",$logfile,$logging);
 my @raw_pids = `$pidgetter`;
 chomp(@raw_pids);
 
 foreach my $raw_pid (@raw_pids) {
  $raw_pid =~ s/^\s+//;
  $raw_pid =~ /^(\d+)\s/;
  $raw_pid = $1;
  my $pid = stripNonNumber($raw_pid);
  my $exists = kill("0",$pid);
  if ($exists) {
   my $message = "checking for running program $program (+$grep) I found one with pid ${pid}. ";
   &log("n/a","n/a",$message,$logfile,$logging);
   $return_pid = $pid
  }
 } # end foreach
 
 return ($return_pid,$filesize);

} # end sub checkPid

sub checkPid2 {
 # gets the PID of the process that matches a SINGLE criteria (different than checkPid since that one checks for two criteria)

 my ($text) = shift;
 my $return_pid = "";
 
 my $pidgetter = "/bin/ps ax -o pid,command | /usr/bin/grep '$text' | /usr/bin/grep -v 'grep' | /usr/bin/grep -v 'sh -c'";
 #print "checking for running program with $pidgetter\n";
 &log("n/a","n/a","checking for any running program with $pidgetter",$logfile,$logging);
 my @raw_pids = `$pidgetter`;
 chomp(@raw_pids);
 
 foreach my $raw_pid (@raw_pids) {
  $raw_pid =~ s/^\s+//;
  $raw_pid =~ /^(\d+)\s/;
  $raw_pid = $1;
  my $pid = stripNonNumber($raw_pid);
  my $exists = kill("0",$pid);
  if ($exists) {
   my $message = "checking for any program operating on $text. I found one with pid ${pid}. ";
   &log("n/a","n/a",$message,$logfile,$logging);
   $return_pid = $pid
  }
 } # end foreach
 
 return ($return_pid);

} # end sub checkPid2

sub process_check {
 # checks if a processor-intense process is already running, so that we can wait til its down before starting another

 my ($program,$grep,$id,$temp_filepath,$actions,$email_alerts,$sendmail,$contact_email,$contact_name) = @_;
 my $process_check_interval = 20; # the number of seconds to wait in between process checks (to see if our file is in the midst of uploading)
 my $process_check_loop_timeout = 100; # the number of times we want to check if a file is being uploaded - this prevents an infinite loop if a process is hung
 my ($grep_output,$filesize) = &checkPid($program,$grep);
 my $loop_check = 0;
 my $fail = 0;
 while ($grep_output ne "") {
  $loop_check++;
  sleep $process_check_interval;
  ($grep_output,$filesize) = &checkPid($program,$grep);
  &log($id,"n/a","$program is already running waiting $process_check_interval secs ($loop_check) output: $grep_output",$logfile,$logging);
  
  if ($loop_check >= $process_check_loop_timeout) {
   # this will hopefully prevent an infinite loop in the event a process is hung
   my $message = "process checking timeout reached - aborting.  Process check interval: $process_check_interval looped: $loop_check output: $grep_output\n";
   &log($id,"n/a",$message,$logfile,$logging);
   $fail = 1
   &fail($id,$temp_filepath,$message);
   if (($email_alerts = 'Yes') && ($action ne 'test')) { &emailAlert($sendmail,"ERROR",$message,"",$contact_email,$contact_name); }
   exit 1;
  }
 }
 return 1;

} # end sub process_check

#################################################################################################
###################                ITUNES-SPECIFIC SUBROUTINES           #####################
#################################################################################################

sub iTunes {
 # handles vairous itunes tasks that will only work on a mac os x
  
 my ($file,$add_to_itunes_playlists,$add_to_itunes,$itunes_bookmarkable) = @_;

 # check that we're on Darwin and only do this stuff if we are
 # and check that itunes is running
 my $darwin = checkForDarwin();
 my $itunes_running = checkForiTunes();
 if ( ($darwin) && ($add_to_itunes eq 'Yes') && ($itunes_running) ) {

  # if the files exists, add to itunes library
  my $finder;
  if (-e $file) {
   &log("n/a","n/a","adding $file to iTunes Library",$logfile,$logging);
   $finder = addToiTunes($file);
  }
  
  # add it to a itunes playlist(s)
  if ( ($add_to_itunes_playlists) && ($finder) ) {
   
   # split the string into an array on commas (can contain multiple playlists)
   my @playlists = split(/,/,$add_to_itunes_playlists);
   
   # loop thru the playlists...
   foreach my $itunes_playlist (@playlists) {
   
    $itunes_playlist = trim(stripNonAlph($itunes_playlist));
    # check if the pl exists
    my $pl_check = checkPlaylistExists($itunes_playlist);
    if ( (not($pl_check =~/^\d/)) || (not($pl_check >= 1 )) ) {
     # create the playlist
     $pl_check = createPlaylist($itunes_playlist);
    } # end if for playlist does not exist
    
    if (($pl_check =~/^\d/) && ($pl_check >=1 )) {
     # check to make sure playlist(s) exist
     &log("n/a","n/a","adding file $file ($finder) to $itunes_playlist playlist",$logfile,$logging);
     my $return = addToPlaylist($finder,$itunes_playlist);
    } # end if for playlist exists
   
   } # end loop thru itunes playlists
  } # end if for add to playlists
  
  # set the track to bookmarkable
  if (($finder) && ($itunes_bookmarkable eq 'Yes')) {
   &log("n/a","n/a","setting file $file ($finder) to bookmarkable/no shuffle",$logfile,$logging);
   &setBookMarkable($finder);
  }
 
 } # end if for is a mac and wants stuff added to lib
 
 return 1;

} # end sub iTunes

sub checkForDarwin {
 # checks that we're running darwin
 
 my $darwin = `/usr/bin/uname -s`;
 $darwin = trim(stripNonAlph(lc($darwin)));
 if ($darwin =~ /^darwin/) {
  return 1;
 } else {
  return 0;
 }

} # end sub for check for darwin

sub addToiTunes {
 # adds the file to the itunes library
 
 my $file = shift;
 my $ref = `/usr/bin/osascript -e 'tell application "iTunes" to add POSIX file "$file"'`;
 $ref = &stripNonAlph($ref);
 sleep 60; # sleep so we can allow itunes to process it.  takes a while for larger files
 return ($ref);
 
} # end sub addToiTunes

sub addToPlaylist {
 # adds the file to a particular playlist
 
 my ($finder,$playlist) = @_;
 my $ref=`/usr/bin/osascript <fetchrow_arrayref) {
   $del_title = $$name_ref[0];
  }
  # split the string into an array on commas (can contain multiple playlists)
  my @playlists = split(/,/,$add_to_itunes_playlists);
  
  # loop thru the playlists...
  foreach my $itunes_playlist (@playlists) {
  
   $itunes_playlist = trim(stripNonAlph($itunes_playlist));
   # check if the pl exists
   my $pl_check = checkPlaylistExists($itunes_playlist);
   if (($pl_check =~/^\d/) && ($pl_check >=1 )) {
    my $ret = &deletePlaylistTrack($itunes_playlist,$del_title);
    &log($program_id,"n/a","deleted $del_title from playlist $itunes_playlist",$logfile,$logging);
   } # end if for playlist exists
   
  } # end foreach thru playlists
 } # end if for $add_to_itunes_playlists 
     
} # end sub playlistTrackDeleteCheck

sub deletePlaylistTrack {
 # delete track from playlist
 # part of "cleanup"
 
 my ($playlist,$songname) = @_;
 my $delete = "";
 if (-e "/usr/bin/osascript") {
  $delete=`/usr/bin/osascript <prepare("$query"); 
 #print $query;
 $sth->execute;
 #print $dbh->err; 
 my $err = $dbh->err;  
 my $errstr = $dbh->errstr;
 if ((length($errstr)>0)) {
  &emailAlert($sendmail,$err,$errstr,$query,$webmaster);
 }

 return $sth;
 
} # end sub db_query

sub stripChars {

 my($text) = @_;
 
 $text =~ s/\n/ /g; # strip carraige returns
 $text =~ s/\t/ /g; # strip tabs
 $text =~ s/\a/ /g; # strip carraige returns
 $text =~ s/"/'/g; # strip quotes and replace with single quotes
 $text =~ s/\s+/ /g; # strip repeating spaces and replace with one
 return ($text);

} # end sub stripchars

sub stripSystemResponse {

 my($text) = @_;
 if ($text) {
  $text =~ s/([^A-Za-z0-9\/])//g; 
 }
 return ($text);

} # end sub stripSystemResponse

sub stripNonNumber {

 my($text) = shift;
 $text =~ s/([^0-9])//g;
 return ($text);

} # end sub strip non number

sub stripNonAlph {

 my($text) = shift;
 $text =~ s/([^0-9a-zA-z ])//g;
 return ($text);

} # end sub strip non alph

sub stripDateSpec {

 my $text = shift;
 if ($text =~ /%/) {
  $text =~ m/^(.*?)%/;
  $text = $1;
 } 
 $text =~ s/^\s+//; # strip leading and trailing spaces
 $text =~ s/\s+$//;
 $text =~ s/[^\w]$//; # strip out any trailing characters that aren't letter or numbers
 $text =~ s/\s+$//; # make double sure the last char isn't a space
 return $text;

} # end sub stripdatespec

sub replaceSpace {

 my($text) = shift;
 #$text = s/(%[abcDdefHhIijklMmprSsTUuVvWwXxYy])//g; # strip out mysql date specifiers
 $text =~ s/([^\w+\s+])//g;
 $text =~ s/^\s+//;
 $text =~ s/\s+$//;
 $text =~ s/([\s+])/_/g;
 return ($text);

} # end sub replacespace

sub trim {

 my $text = shift;
 $text =~ s/^\s+//;
 $text =~ s/\s+$//;
 return $text;
}

sub round {
    my($number) = shift;
    return int($number + .5);
}

sub log {

 my ($id,$title,$message,$logfile,$logging) = @_;
 my $timestamp = strftime( "%Y-%m-%d %H:%M:%S",localtime(time));
 if (($message) && ($logfile) && ($logging)) {
  $message =~ s/\n/ /g;
  $message =~ s/\s+/ /g;
  open(FILE,">>$logfile") || die "could not open $logfile for writing $!";
  print FILE "$timestamp\t$id\t$title\t$message\n";  
  close(FILE);
  return 1;
 } else {
  return 0;
 }
 
} #end sub log

sub emailAlert {

 my ($sendmail,$alert,$message,$sql,$contact_email,$contact_name) = @_;
 my $timestamp = strftime( "%Y%m%d%H%M%S",localtime(time));
 my $subject = "StreamShifter Alert - $alert\n";
 my $contact = $contact_email;
 if ($contact_name) { $contact = $contact_name; }
 my $email_content = "$contact,\n\n$alert: $message\n$sql\nSincerely,\n\nStreamShifter\n\nTimestamp: $timestamp\n\n";
 if (&Mail($sendmail,$contact_email,$subject,$email_content)) {
  #print "alert below.  Emailed $contact_email\n\n$alert: $message\n";
 } else {
  #print "tried to email $contact_email about alert but was unsuccessful\n";
 }

} # end sub emailAlert

sub Mail {

my ($Sendmail,$recipient,$subject,$email_content,$from) = @_;
if (!$from) {
 $from = $recipient;
}
open(MAIL, "| $Sendmail -f $from -t") || die "Couldn't open a pipe to Sendmail: [$Sendmail].  Please notify $from of this error.";
print MAIL "From: $from\n";
print MAIL "To: $recipient\n";
print MAIL "Subject: $subject\n\n";
print MAIL <<"EOF";
$email_content\n
EOF
close(MAIL);

}

# for getting the basename of a file without using a module
sub basename {
 my $file = shift;
 $file =~ s!^(?:.*/)?(.+?)(?:\.[^.]*)?$!$1!;
 return $file;
} # end sub basename

################# NOT USED AT THIS TIME ##############
sub adjustTime {

 my ($hour,$minute) = @_;
 
 # remove leading zeros from minute and hour so we can do math on them
 if ($minute =~ /^0([0-9])$/) {
  $minute = $1;
 }
 if ($hour =~ /^0([0-9])$/) {
  $hour = $1;
 }
 # if the minute is 0, subtract 1 from the minute.  if the minute is 0 and the hour is 0, the hour becomes 23.
 if ($minute == 0) {
  $minute = 59;
  if ($hour == 0) {
   $hour = 23;
  } else {
   $hour = ($hour-1);
  }
 } else {
  $minute = ($minute-1);
 }
 # put the leading zeros back on single digits
 if ($minute =~ /^([0-9]{1})$/) {
  $minute = '0' . $1;
 }
 if ($hour =~ /^([0-9]{1})$/) {
  $hour = '0' . $1;
 }
 return($hour,$minute); 
 
} # end sub adjustTime

# EOF

Finally, it needs a cronjob to run it. Edit the path and add to your crontab:
* * * * * /home/user/bin/streamshifter.cgi -s
Once you have it set up, you admin the programs in the MySQL database (see the test data in the "streamshifter_schedule" table. There are also a few things you can do on the command line with it. run:
./streamshifter.cgi -h
To see the options.