#!/usr/bin/perl
# Last.fm Info Plugin for Pidgin / libpurple
# Copyright (c) 2008 by Dominik George <pidgin-lastfm@naturalnet.de>
#
# Yippie, my first perl script :-D
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

use Purple;

# Global variables
our $song_joker;
our $ago_joker;
our $time_joker;

# Last.fm API
our $apikey = '4b9aa27d34af5238708afa6807e6a18a';

# Store plugin reference globally
our $plugin;
our $act_timeout;

# Set the current version
our $actversion = "0.4a";

# Information on plugin for libpurple
%PLUGIN_INFO = (
 perl_api_version => 2,
 name => "Last.fm Info Plugin",
 version => $actversion,
 summary => "Display Last.fm info in status message",
 description => "Plugin to display information from your Last.fm profile in Pidgin",
 author => "Dominik George <pidgin-lastfm\@naturalnet.de>",
 url => "http://pidgin-lastfm.naturalnet.de",
 
 # Subs that libpurple has to know
 load => "plugin_load",
 unload => "plugin_unload",
 prefs_info => "prefs_info_cb",
 plugin_action_sub => "plugin_actions_cb"
);

# Called upon plugin initialization
sub plugin_init {
 return %PLUGIN_INFO;
}

# Get everything ready upon loading
sub plugin_load {
 $plugin = shift;
 Purple::Debug::info("lastfm", "Last.fm Plugin loading ...\n");

 # Load or create preferences
 Purple::Prefs::add_none("/plugins/core/lastfm");
 Purple::Prefs::add_string("/plugins/core/lastfm/username", "");
 Purple::Prefs::add_string("/plugins/core/lastfm/content", "user.getRecentTracks");
 Purple::Prefs::add_int("/plugins/core/lastfm/timeout", 30);
 Purple::Prefs::add_bool("/plugins/core/lastfm/nowplayingonly", FALSE);

 # Song info cache from preferences
 Purple::Prefs::add_string("/plugins/core/lastfm/song_joker", "%s");
 Purple::Prefs::add_string("/plugins/core/lastfm/ago_joker", "%a");
 Purple::Prefs::add_string("/plugins/core/lastfm/time_joker", "%t");

 # Setup timeout for refreshing info
 our $act_timeout = Purple::timeout_add($plugin, 1, \&loadinfo_cb, $plugin);

 Purple::Debug::info("lastfm", "Last.fm Plugin loaded.\n");
}

# Cleanly unload plugin
sub plugin_unload {
 my $plugin = shift;

 # Reset status message so we find the standard placeholder next time
 # set_status("%s", "%a", "%t");

 Purple::Debug::info("lastfm", "Last.fm Plugin removed.\n");
}

# Preferences dialog box
sub prefs_info_cb {
 my $frame = Purple::PluginPref::Frame->new();
 
 # Audioscrobbler username
 my $ppref = Purple::PluginPref->new_with_name_and_label("/plugins/core/lastfm/username", "Username:");
 $ppref->set_type(2);
 $ppref->set_max_length(16);
 $frame->add($ppref);

 # Desired content
 $ppref = Purple::PluginPref->new_with_name_and_label("/plugins/core/lastfm/content", "Content:");
 $ppref->set_type(1);
 $ppref->add_choice("Most recent track", "user.getRecentTracks");
 $ppref->add_choice("Top track", "user.getTopTracks");
 $ppref->add_choice("Most recently loved track", "user.getLovedTracks");
 $ppref->add_choice("Most recently banned track", "user.getBannedTracks");
 $frame->add($ppref);

 # Timeout for refreshing content
 $ppref = Purple::PluginPref->new_with_name_and_label("/plugins/core/lastfm/timeout", "Refresh (sec):");
 $ppref->set_type(3);
 $ppref->set_bounds(10, 300);
 $frame->add($ppref);

 # Don't show information when not "now playing"?
 $ppref = Purple::PluginPref->new_with_name_and_label("/plugins/core/lastfm/nowplayingonly", "Show --- when not listening right now");
 $ppref->set_type(1);
 $frame->add($ppref);

 return $frame;
}

# Actions that user can run from UI menu
sub plugin_actions_cb {
 my @actions = ("Refresh info", "Reset placeholders", "Check for updates", "Credits");
}

%plugin_actions = (
 "Refresh info" => \&loadinfo_cb,
 "Reset placeholders" => \&reset_placeholders,
 "Check for updates" => \&checkupdate_cb,
 "Credits" => \&loadcredits_cb
);

# Helper function to escape strings for regex compatibility
sub regex_escape {
 my $string = $_[0];

 $string =~ s/\\/\\\\/g;
 $string =~ s/\//\\\//g;
 $string =~ s/\./\\./g;
 $string =~ s/\+/\\+/g;
 $string =~ s/\?/\\?/g;
 $string =~ s/\^/\\^/g;
 $string =~ s/\$/\\\$/g;
 $string =~ s/\|/\\|/g;
 $string =~ s/\(/\\(/g;
 $string =~ s/\)/\\)/g;
 $string =~ s/\[/\\]/g;
 $string =~ s/\]/\\]/g;
 $string =~ s/\{/\\{/g;
 $string =~ s/\}/\\}/g;

 return $string;
}

# Load placeholders from preferences
sub load_placeholders {
 our $song_joker = Purple::Prefs::get_string("/plugins/core/lastfm/song_joker");
 our $ago_joker = Purple::Prefs::get_string("/plugins/core/lastfm/ago_joker");
 our $time_joker = Purple::Prefs::get_string("/plugins/core/lastfm/time_joker");
}

# Save current placeholders to the preferences
# I hope we will survive SEGFAULTs that way :-)
sub save_placeholders {
 Purple::Prefs::set_string("/plugins/core/lastfm/song_joker", $song_joker);
 Purple::Prefs::set_string("/plugins/core/lastfm/ago_joker", $ago_joker);
 Purple::Prefs::set_string("/plugins/core/lastfm/time_joker", $time_joker);
}

# Reset placeholders to the defaults
sub reset_placeholders {
 load_placeholders();

 our $song_joker = "%s";
 our $ago_joker = "%a";
 our $time_joker = "%t";

 save_placeholders();
}

sub parse_credits_cb {
 my $credits = shift;

 Purple::Notify::message($plugin, 2, "Last.fm Plugin Credits", "The following people helped a lot at developing the plugin:", $credits, NULL, NULL);
}

sub loadcredits_cb {
 my $plugin = shift;
 my $url = "http://pidgin-lastfm.naturalnet.de/res/credits.txt";
 Purple::Util::fetch_url($plugin, $url, TRUE, "Mozilla/5.0", TRUE, \&parse_credits_cb);
}

sub checkupdate_cb {
 my $plugin = shift;

 # URL with text file containing version information
 my $url = "http://pidgin-lastfm.naturalnet.de/res/version.txt";

 # Fetch version information
 Purple::Util::fetch_url($plugin, $url, TRUE, "Mozilla/5.0", TRUE, \&parse_update_cb);
}

sub parse_update_cb {
 my $version = shift;

 my $linebreak = index($version, "\n");
 $version = substr($version, 0, $linebreak);

 Purple::Debug::info("lastfm", "The running version is " . $actversion . ".\n");
 Purple::Debug::info("lastfm", "The most recent version is " . $version . ".\n");

 if ($version > $actversion) {
  Purple::Notify::message($plugin, 2, "Last.fm Plugin", "New version available!", "A new version of the Pidgin Last.FM plugin (version " . $version . ") is available for download.", NULL, NULL);
 } else {
  if ($actversion > $version) {
   Purple::Notify::message($plugin, 2, "Last.fm Plugin", "Your version is from the future!", "You are running a more recent version than the server knows.\n\nI conclude: You are either a developer or a magician :-D ...", NULL, NULL);
  } else {
   Purple::Notify::message($plugin, 2, "Last.fm Plugin", "No new version available!", "You are running the most recent version of the plugin.", NULL, NULL);
  }
 }
}

sub set_status {
 my $song = $_[0];
 my $ago  = $_[1];
 my $time = $_[2];

 # Parse status message and set info
 my $status  = Purple::SavedStatus::get_current();
 my $message = Purple::SavedStatus::get_message($status);
 my $oldmsg  = $message;

 load_placeholders();

 my $song_regex = regex_escape($song_joker);
 my $ago_regex = regex_escape($ago_joker);
 my $time_regex = regex_escape($time_joker);

 Purple::Debug::info("lastfm", "Looking for song joker \"" . $song_joker . "\".\n");

 if ($message =~ /$song_regex/i) {
  Purple::Debug::info("lastfm", "Old message was \"" . $message . "\".\n");
 
  # Now do the regex replacements

  if (($message =~ /$song_regex/i) && ($song ne "") && ($song_joker ne $song)) {
   $message =~ s/$song_regex/$song/i;
   our $song_joker = $song;
  }
 
  if (($message =~ /$ago_regex/i) && ($ago ne "") && ($ago_joker ne $ago)) {
   $message =~ s/$ago_regex/$ago/i;
   our $ago_joker = $ago;
  }
 
  if (($message =~ /$time_regex/i) && ($time ne "") && ($time_joker ne $time)) {
   $message =~ s/$time_regex/$time/i;
   our $time_joker = $time;
  }
 
  save_placeholders();
 
  # Only update if something has changed
  if ($message ne $oldmsg) {
   Purple::Debug::info("lastfm", "Changing to \"" . $message . "\".\n");
   Purple::SavedStatus::set_message($status, $message);
   Purple::SavedStatus::activate($status);
  } else {
   Purple::Debug::info("lastfm", "Status message unchanged.\n");
  }
 } else {
  Purple::Debug::info("lastfm", "Song joker not found ...\n");
  Purple::Debug::info("lastfm", "If you think this is wrong, try resetting the song jokers!\n");
 }

 # Reset timeout so refresh is called regularly 
 my $timeout = Purple::Prefs::get_int("/plugins/core/lastfm/timeout");
 our $act_timeout = Purple::timeout_add($plugin, $timeout, \&loadinfo_cb, $plugin);
 Purple::Debug::info("lastfm", "Reset timeout to ".$timeout." seconds.\n");
}


sub parse_data_cb {
 my $data = shift;
 Purple::Debug::info("lastfm", "Callback function for HTTP request called.\n");

 my $artist = "";
 my $title     = "";
 my $timestamp = 0;

 if ($data =~ m/<error code="\d*">(.*?)<\/error>/gis) {
  $error = $1;
  Purple::Debug::error("lastfm", "Huh? We got an error message from the server ...\n");
  Purple::Debug::error("lastfm", $error."\n");
  Purple::Debug::error("lastfm", "Skipping status message changing.\n");

  # Reset timeout so refresh is called regularly 
  my $timeout = Purple::Prefs::get_int("/plugins/core/lastfm/timeout");
  our $act_timeout = Purple::timeout_add($plugin, $timeout, \&loadinfo_cb, $plugin);
  Purple::Debug::info("lastfm", "Reset timeout to ".$timeout." seconds.\n");
 } else {
  # Parse string as XML

  my $nowplaying = 0;

  if ($data ne "") {
   my @lines = split(/\n/, $data);
 
   foreach (@lines) {
    my $line = $_;
 
    if ($line =~ /^\s*<artist mbid=.*?>(.*?)<\/artist>$/) {
     $artist = $1;
    } elsif ($line =~ /^\s*<name>(.*?)<\/name>$/) {
     $title = $1;
    } elsif ($line =~ /^\s*<date uts="(\d*)".*$/) {
     $timestamp = $1;
    } elsif ($line =~ /nowplaying="true"/) {
     $nowplaying = 1;
    }
 
    if ($line =~ /^\s*<\/track>$/) {
     last;
    }
   }
  }

  $song = $artist . " - " . $title;

  # Play with the time :)
  my $ago = "";

  if ($nowplaying == 0) {
   my $time = time;
   my $diff = $time - $timestamp;
   Purple::Debug::info("lastfm", "Current time: " . $time . "\n");
   Purple::Debug::info("lastfm", "Srobbled at : " . $timestamp . "\n");
   Purple::Debug::info("lastfm", "Difference  : " . $diff . "\n");

   my $days = int($diff / 86400);
   $diff %= 86400;

   my $hours = int($diff / 3600);
   $diff %= 3600;

   my $minutes = int($diff / 60);

   if ($days > 0) {
    $ago .= $days . " day";

    if ($days > 1) {
     $ago .= "s";
    }
   }

   if ($hours > 0) {
    if ($days > 0) {
     $ago .= ", ";
    }

    $ago .= $hours . " hour";

    if ($hours > 1) {
     $ago .= "s";
    }
   }

   if ($minutes > 0) {
    if (($hours > 0) || ($days > 0)) {
     $ago .= ", ";
    }

    $ago .= $minutes . " minute";

    if ($minutes > 1) {
     $ago .= "s";
    }
   }

   $ago .= " ago";
  } else {
   $ago = "right now";
  }

  $timestring = localtime($timestamp);

  Purple::Debug::info("lastfm", "Song is \"" . $song . "\"\n");
  Purple::Debug::info("lastfm", "Scrobbled " . $ago. ".\n");
  Purple::Debug::info("lastfm", "This was at " . $timestring . ".\n");

  my $nponly = Purple::Prefs::get_bool("/plugins/core/lastfm/nowplayingonly");
  if ($nponly && (! ($nowplaying == 1))) {
   $song = "---";
  }

  # Call method that will hopefully set our new status message
  set_status($song, $ago, $timestring);
 }
}

# Routine that is called in any case that triggers a refresh
sub loadinfo_cb {
 my $plugin = shift;
 Purple::Debug::info("lastfm", "User or timeout requested to refresh info.\n");

 # Find out what we must fetch
 my $url = build_url();

 # Let libpurple do the work
 Purple::Debug::info("lastfm", "Telling libpurple to fetch data.\n");
 Purple::Debug::info("lastfm", "URL to be fetched: " . $url . "\n");
 Purple::Util::fetch_url($plugin, $url, TRUE, "Mozilla/5.0", TRUE, \&parse_data_cb);

 # Stop timeout for now, will be reinstated by set_status
 return false;
}

# Build URL to retrieve from Audioscrobbler
sub build_url {
 my $protocol = "http://";
 my $hostname = "ws.audioscrobbler.com";
 my $username = Purple::Prefs::get_string("/plugins/core/lastfm/username");
 my $content = Purple::Prefs::get_string("/plugins/core/lastfm/content");
 my $uri = "/2.0/?method=" . $content ."&user=". $username . "&api_key=" . $apikey;
 my $url = $protocol . $hostname . $uri;

 return $url;
}
