#!/usr/bin/perl # Last.fm Info Plugin for Pidgin / libpurple # Copyright (c) 2008 by Dominik George # # 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 ", 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>/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>$/) { $artist = $1; } elsif ($line =~ /^\s*(.*?)<\/name>$/) { $title = $1; } elsif ($line =~ /^\s*$/) { 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; }