aboutsummaryrefslogtreecommitdiffstats
path: root/.irssi/scripts/lastfm.pl
blob: 4ff2105efb94ecfcb7b0e52a1c74e30424b6bbce (plain)
1
2
3
4
5
6
7
8
9
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# vim: set expandtab:
use vars qw($VERSION %IRSSI);
$VERSION = "5.8";
%IRSSI = (
        authors     => "Simon 'simmel' Lundström",
        contact     => 'simmel@(freenode|quakenet|efnet) http://last.fm/user/darksoy',
        name        => "lastfm",
        date        => "20110125",
        description => 'A now-playing-script which uses Last.fm',
        license     => "BSD",
        url         => "http://soy.se/code/",
);
# USAGE
# For details on how to use each setting, scroll down to the SETTINGS section.

# QUICK START
# * First of all, you need the libwww/LWP package installed. The package in
# your package system is probably called something with libwww and perl and/or
# p5 in it.
# * /set lastfm_user to the username that you are using on Last.fm
# * Show with /np or %np<TAB> what song "lastfm_user" last scrobbled to Last.fm via /say. If "lastfm_use_action" is set, it uses /me.
# * To see what another user on Last.fm is playing is also possible via /np <username> or %np(<username>).
# The now-playing message is configurable via via "lastfm_output" (and lastfm_output_tab_complete when using %np, if not set it will use lastfm_output by default.). "lastfm_strftime" can be used to configure the display of date and time when the song was scrobbled.

# SETTINGS
# NOTE: Do not set these options here, use /set <option> <value> in irssi!
# These are just defaults and descriptions on what the options do.

# The username which you are using on Last.fm
Irssi::settings_add_str("lastfm", "lastfm_user", "");

# The output that you want to use.
# The substitution variables are:
#   %artist = Self explanatory
#   %album  = Self explanatory
#   %name   = Name of song*
#   %url    = URL to song on Last.fm
#   %player = Player we are using to submit to Last.fm with. See setting "lastfm_get_player" below
#   %user   = User that is playing, when /np <username> or %np(<username> is used
# If "lastfm_output_tab_complete" is not defined, "lastfm_output" will be used instead.
# Something bothered me for a long time and when something really starts to itch
# I tend to want to do something about it. I'm /np:ing away displaying all sorts
# of tracks to my friends until I get to a track which has no album information
# on Last.fm and the output becomes really ugly "np: Kraftwerk-Aerodynamik 
# (Alex Gopher/Etienne de Crecy dynamik mix) ()". What's with that last ()!? Oh,
# right we are using "np: %artist-%name (%album)" as "lastfm_output". Wouldn't 
# it be really cool if lastfm.pl knew when certain information from Last.fm
# didn't exist and didn't display it? So thought I, so that's why I created a 
# conditional. It works that you have to put your tag (%album e.g.) within %()
# e.g. "np: %artist-%name%( (%album))" and everything between %( and ) only gets
# displayed if the tag inside actually exists! Cool, huh!?

#  *) Name is used instead of, the more logical IMO, track since that is what Last.fm reports in their .xml file that we parse.
Irssi::settings_add_str("lastfm", "lastfm_output", '%(%user is )np: %artist-%name');
Irssi::settings_add_str("lastfm", "lastfm_output_tab_complete", '');

# If we should use /me instead of /say
Irssi::settings_add_bool("lastfm", "lastfm_use_action", 0);

# If we should make the subtitution variable %player available which is very slow to fetch but nice to have.
Irssi::settings_add_bool("lastfm", "lastfm_get_player", 0);

# Changelog#{{{

# 5.8 -- Tue Jan 25 16:11:29 CET 2011
# * Ignore a closure warning

# 5.7 -- Mon Jan 24 16:39:06 CET 2011
# * Fixed a bug where we forked when we still waited for a reply from last.fm

# 5.6 -- Sun Jul 18 13:16:38 CEST 2010
# * Made substitution variable %user available when /np <username> or
# %np(<username>) is used.
# * Made some checks a bit more strict.

# 5.5 -- Mon Jul 12 19:04:26 CEST 2010
# * Rewrote the whole error handling
# * Fixed a bug where the error messages would be said and not printed.
# * Fixed some minor bugs and removed some unneeded code.

# 5.4 -- Wed May 26 17:04:08 CEST 2010
# * Last.fm updated their profile HTML so that the %player macro didn't work.
# Thanks to Keith Ward for mentioning this and suggesting a fix.
# * A minor fix which removes some debug messages when error reporting.

# 5.3 -- 
# * I used POSIX::_exit() but I never did "use POSIX;". Leo Green, mortiis and
# rissy reported this problem, thanks! This is an issue when you have a newer
# version of Perl installed (>5.10)

# 5.2 -- Mon Nov 16 08:25:20 CET 2009
# * When you remove a subroutine you should remove all calls to it..

# 5.1 -- Wed Nov 11 09:39:54 CET 2009
# * Ok, I admit that using undocumented features in an API is bad, but come
# on..  Anyway, fixed now, everything should work as it should and should
# never break again (flw)...

# 5.0 -- Mon Nov 9 08:34:48 CET 2009
# * Fixed a warning reported by mm_mannen and did a yet another clean up

# 4.9 -- Sat Nov 7 18:10:17 CET 2009
# * Last.fm changed how their API behaved and that broke my code because
# I'm a fool and I don't want to use an XML-lib because of your sake (so you
# won't have to install yet another Perl-module). Thanks to supertobbe and 
# mm_mannen who saw and reported this!
# * Fixed so that lastfm_get_player works again and made it say that it
# doesn't work next time Last.fm changes their HTML.
# * Removed the date support in lastfm_output and lastfm_output_tab_complete
# since I use the API another way now.
# * Removed cache. It was broken at times and I can't be arsed to debug it.
# It's not that much faster but the complexity gets bigger. If someone REALLY
# needs this, give me a shout.
# * Removed, rewrote and cleaned up some parts of the script.

# 4.8 -- Sun May 10 10:11:29 CEST 2009
# * Fixed a bug with the cache ('There are only two hard things in 
# Computer Science: cache invalidation and naming things' -Phil Karlton)
# * Started using HTML::Entities for decoding all sorts of HTML-chars, it's 
# included in libwww anyway.

# 4.7 -- Tue Apr  8 13:37:11 CEST 2009
# * Start using LWP::UserAgent instead of LWP::Simple and got rid of the idea to
# start using my own HTTP-lib (it was finished, but..). I'm getting old ; P
# * Made so that everything is cached and checks if the Last-Modified date when 
# getting information from Last.fm.
# * Fixed some documentation bugs.

# 4.6 -- Wed Mar 18 19:45:11 CET 2009
# * Fixed an changed behavour in irssi-trunk with the error handling (which I should replace anyway!).
# * Added %player substitute variable that shows what application you are using to scrobble with. This is very slow, so I made it an option, "lastfm_get_player".
# * Fixed print_raw once and for all (famous last words..) so now debug output looks really neat.
# * Added an quick start which should help get going faster
# * Fixed an issue where %np(lastfmusername) would not work.
# * Fixed error mesages for %np(lastfmusername)
# * Fixed an problem with irssi-svn where die's message have changed. Thanks tto jnpplf  for reporting this.

# 4.5 -- Wed  1 Oct 2008 20:03:47 CEST
# * Removed a debug output
# * Fixed some datacorruption, references in Perl is hard! = (

# 4.4 -- Wed  1 Oct 2008 16:34:34 CEST
# * Changed so that all the tab-commands use % instead of $ so that it's consistent through out the script.
# * Ripped out my sprintf crap and made it more sane. You should use %artist, %album, etc in your nowplaying-setting now. Since sprintf is nolonger used I renamed that setting too.
# * Made everything that you can set in "lastfm_output" tabable so now you can do %artist<TAB>.
# %() in "lastfm_output" really works. It really didn't before.
# * Fixed some issues with the date probably not working, but should now.
# * Made the script check if Last.fm's scrobbler server is alive and kicking before we blame them.

# 4.3 -- Mon 21 Jul 2008 08:46:36 CEST
# * Seem like I misunderstood the protocol. The date/time is only sent when we have scrobbled the track, not when we started to listen to it.

# 4.2 -- Tue 15 Jul 2008 15:40:08 CEST
# Yay! Three new version within a day! (No, I'm not bored at work)
# * Made /np username and $np(username) make username the prefix of np: yadayada or whatever your lastfm_sprintf or lastfm_sprintf_tab_complete is.

# 4.1 -- Tue 15 Jul 2008 15:23:03 CEST
# Well, that version lasted long!
# * Fixed a bug with /np not working.
# * Fixed an issue where debug info would be printed even if lastfm_debug was off.

# 4.0 -- Tue 15 Jul 2008 10:17:51 CEST
# * Fixing a sprintfng-bug which didn't display time if album was not set.
# * Rewrote the whole script to use Last.fm's API which is very accurate. There is no need for $np! and /np! now, so I'm removing them.
# * Cleaned up abit.

# 3.9 -- Fri 11 Jul 2008 21:49:20 CEST
# * Fixing a few bugs noticed by supertobbe

# 3.8 -- Fri 11 Jul 2008 18:21:52 CEST
# * Shaped up error handling and now all error messages are shown.
# * Added a user configurable debug mode, good for sending in bugs and weird behaviour.
# * Minor cleanup

# 3.7 -- Thu 22 May 2008 10:33:55 CEST
# * Fixed so that /np! and $np! fetches the album title too. This is horribly slow and takes approx. 6s on very fast connection. Last.fm isnt very fast I'm afraid and this is not a good way to do it.
# * Cleaned up a few places. Started to look at the error handling and it seems to be alot of work.

# 3.6 -- Tue Nov 13 15:22:37 CET 2007
# * Fixed encoding so that it always the data into the charset that you have specified in "term_charset" which irssi also uses.

# 3.5 -- Mon Nov 12 11:50:46 CET 2007
# * Fixed the regex for parsing Recently Listened Tracks so that it works when listening with the Lastfm client.

# 3.4 -- Fri Nov  9 00:23:40 CET 2007
# * Added /np lastfmusername

# 3.3 -- Tue Nov  6 01:54:59 CET 2007
# * Finally added conditional sprintf-syntax! Let's say you want to use 'np: %s-%s (%s)' as "lastfm_sprintf". If you use /np it works out fine and displays 'np: Boards of Canada-Energy Warning (Geogaddi)' but what if you use /np! then it displays 'np: Boards of Canada-Energy Warning ()' since /np! can't get the album information. Doesn't that look ugly? Meet conditional sprintf. Now set your "lastfm_sprintf" to 'np: %s-%s%( (%s))'. ' (%s)' will only be printed if we get a third value, the album name in this case. Smart, huh? Big thanks to rindolf, apeiron and Khisanth from #perl@freenode for help with scoping with global variables.
# * Also added "lastfm_sprintf_tab_complete" which makes, if set, $np<TAB> use a different sprintf pattern than /np. Will default back to "lastfm_sprintf".

# 3.2 -- Wed Oct 24 23:07:01 CEST 2007
# * I don't like dependencies and I really wonder why I lastfm depended on DateTime. I remember now that it was morning and I was really tired when I coded it. Anyway, it's removed now along with Socket and URI::Escape. I'll try to remove the dependency for libwww later on.

# 3.1 -- Sun Oct 21 22:52:36 CEST 2007
# * Added /np! and $np! to use the "lastfm_be_accurate_and_slow" method without having to change the setting.

# 3.0 -- Fri Oct 19 14:26:03 CEST 2007
# * Created a new setting "lastfm_be_accurate_and_slow" which makes lastfm.pl parse your profile page to check what song you are playing right now. But be warned, this is slow and horrible (like my code! ; ). But it works until Last.fm makes this data available through their Web Services. This disables the album and "scrobbled at" features of "lastfm_sprintf" so you have to adapt it if you don't want it to look weird. I'm working on a new implementation of printf which allows for conditions but it took more time than I thought and time is something that I don't have much of ='(

# 2.5 -- Tue Oct  9 11:29:56 CEST 2007
# * Fixed the encoding issue by converting from Last.fms UTF-8 into Perls internal encoding. With $np<TAB> output will be looking UTF-8-in-latin1 if you don't have an UTF-8 enabled Terminal, but it will display correctly after you have sent it.

# 2.4 -- Mon Oct  8 16:08:09 CEST 2007
# * Fixed an error in error reporting ; P Bug noticed by supertobbe = *
# * I should make an more generic and better error reporting.

# 2.3 -- Sat Oct  6 16:38:34 CEST 2007
# * Made /np a nonblocking operation. Irssi's fork handling is REALLY messy. Thanks to tss and tommie for inspiring me in their scripts. $np cannot be made nonblocking, I'm afraid (patches welcome).
# * Cleaned up abit.

# 2.2 -- Sat Aug 18 02:20:44 CEST 2007
# * Now you can use $np(darksoy) to see what I play (or someone else for that matter ; ).

# 2.1 -- Tue Jul 17 12:50:18 CEST 2007
# * Now you can use $np or $nowplaying as a tab-completion too, but a warning here, this is a blocking action so irssi won't respond or be usable until it is finished or the timeout is hit.
# * Abstracted it abit more so that it can be used in more ways, ex. for the reason above.

# 2.0 -- Fri Jun 29 10:38:32 CEST 2007
# * Now you can show the time that the song was submitted in lastfm_sprintf. Added lastfm_strftime to configure how the date is presented.
# * Added $lastfm and $lfm as tab-completions to your own Last.fm profile URL. Ripoff of Jured's guts.pl (http://juerd.nl/irssi/)

# 1.5 -- Sat May 12 03:30:24 CEST 2007
# * Started using XML instead because we get more info from it, like album (but it's often wrong).

# 1.0 -- Thu Apr 12 16:57:26 CEST 2007
# * Got fedup with no good Last.fm-based now playing scripts around.

# THANKS
# Random individuals on #perl@freenode, could namedrop icke, 
# }}}

# TODO
# You tell me!

# Move along now, there's nothing here to see.

sub DEBUG {
  Irssi::settings_add_bool("lastfm", "lastfm_debug", 0);
  Irssi::settings_get_bool("lastfm_debug");
};

use strict;
use warnings;
no warnings 'closure';
use Data::Dumper;
use Encode;
use HTML::Entities;
use Irssi;
use LWP::UserAgent;
use POSIX;

my $pipe_tag;
my $waiting_for_reply;
my $api_key = "eba9632ddc908a8fd7ad1200d771beb7";
my $fields = "(artist|name|album|url|player|user)";
my $ua = LWP::UserAgent->new(agent => "lastfm.pl/$VERSION", timeout => 10);

sub lastfm_nowplaying {
  my ($content, $url, $response, $tag, $value, %data);
  my ($user_shifted, $is_tabbed, $nowplaying, $witem) = @_;
  my $user = $user_shifted || Irssi::settings_get_str("lastfm_user");
  $nowplaying ||= ((Irssi::settings_get_str("lastfm_output_tab_complete") ne "" && $is_tabbed) ? Irssi::settings_get_str("lastfm_output_tab_complete") : Irssi::settings_get_str("lastfm_output"));

  my $command_message = ($is_tabbed) ? '%%np(username)' : '/np username';
  if ($user eq '') {
    return "ERROR: You must /set lastfm_user to a username on Last.fm or use $command_message";
  }

  if ($nowplaying =~ /^%(lastfm|lfm)$/) {
    return "http://last.fm/user/$user/";
  }
  elsif ($nowplaying =~ /^%user$/) {
    return $user;
  }

  $data{'user'} = $user if ($user_shifted);

  $url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=$user&api_key=$api_key&limit=1";
  print Dumper "Checking for scrobbles at: $url" if DEBUG;
  $response = $ua->get($url);
  $content = $response->content;

  # TODO This should work, untested (fail more Last.fm! ; )
  if ($content =~ m!<lfm status="failed">.*<error .*?>([^<]+)!s) {
    return "ERROR: $1";
  }
  my @data = split('\n', $content);

  if (!grep(m!<track nowplaying="true">!, @data)) {
    print Dumper \$response if DEBUG;
    print Dumper \$content if DEBUG;
    return "ERROR: You are not playing anything according to Last.fm. Check http://www.last.fm/user/$user and see if they turn up there, otherwise restart your scrobbler.";
  }

  my $regex = qr!<$fields.*?(?:uts="(.*?)">.*?|>(.*?))</\1>!;

  foreach my $data (@data) {
    if ($data =~ m!</track>!) {
      last;
    }
    elsif ($data =~ /$regex/) {
      ($tag, $value) = ($1, (defined($2) ? $2 : $3));
      print Dumper \$tag, \$value, \$data if DEBUG;
      $data{$tag} = $value;
    }
  }

  if (Irssi::settings_get_bool("lastfm_get_player")) {
    $url = "http://www.last.fm/user/$user";
    $content = $ua->get($url)->content;
    if ($content =~ m!<span class="source">(.*?)</span>!) {
      $_ = $1;
      s/<[^>]*>//mgs;
      $data{'player'} = $_;
    }
    else {
      print "Couldn't find the player even though lastfm_get_player was set" if DEBUG;
    }
  }

  print Dumper \%data if DEBUG;
  print Dumper "Output pattern before: $nowplaying" if DEBUG;
  $nowplaying =~ s/(%\((.*?%(\w+).?)\))/($data{$3} ? $2 : "")/ge;
  print Dumper "Output pattern after: $nowplaying" if DEBUG;
  $nowplaying =~ s/%$fields/$data{$1}/ge;
  decode_entities($nowplaying);
  Encode::from_to($nowplaying, "utf-8", Irssi::settings_get_str("term_charset"));
  return $nowplaying;
}

sub lastfm_blocking {
  my ($witem, $user) = @_;
  my $nowplaying = lastfm_nowplaying($user, undef, undef, $witem);
  lastfm_print($witem, $nowplaying);
}

sub lastfm_forky {
  my ($witem, $user) = @_;
  if ($waiting_for_reply) {
    lastfm_print(Irssi::active_win(), "We are still waiting for Last.fm to return our results");
    return;
  }
  # pipe is used to get the reply from child
  my ($rh, $wh);
  pipe($rh, $wh);

  # non-blocking host lookups with fork()ing
  my $pid = fork();
  if (!defined($pid)) {
    Irssi::print("Can't fork() - aborting");
    close($rh);
    close($wh);
    return;
  }

  $waiting_for_reply = 1;

  if ($pid > 0) {
    # parent, wait for reply
    close($wh);
    Irssi::pidwait_add($pid);
    $pipe_tag = Irssi::input_add(fileno($rh), INPUT_READ, \&pipe_input, [$witem, $rh]);
    return;
  }

  my $text;
  eval {
    # child, do the lookup
    $text = lastfm_nowplaying($user);
  };

  if (!$text) {
    $text = "ERROR: Error message: $!";
  }

  eval {
    # write the reply
    print($wh $text);
    close($wh);
  };
  POSIX::_exit(1);
}


sub pipe_input {
  my ($witem, $rh) = @{$_[0]};
  my $text = <$rh>;
  close($rh);

  Irssi::input_remove($pipe_tag);
  $pipe_tag = -1;
  undef $waiting_for_reply;

  lastfm_print($witem, $text);
}

sub lastfm_print {
  my ($witem, $text, $tabbed) = @_;
  # Fugly error handling
  if ($text =~ s/^ERROR: //) {
    Irssi::active_win()->print($text);
    return;
  }

  if ($tabbed) {
    return $text;
  }
  elsif (defined $witem->{type} && $witem->{type} =~ /^QUERY|CHANNEL$/) {
    if (Irssi::settings_get_bool("lastfm_use_action")) {
      $witem->command("me $text");
    }
    else {
      $witem->command("say $text");
    }
  }
  else {
    Irssi::active_win()->print($text);
  }
}

Irssi::command_bind('np', sub {
    my ($data, $server, $witem) = @_;
    $data =~ s/ .*//;
    $data ||= 0;
    if (DEBUG) {
      lastfm_blocking($witem, $data);
    }
    else {
      lastfm_forky($witem, $data);
    }
  }, 'lastfm');

Irssi::signal_add_last 'complete word' => sub {
  my ($complist, $window, $word, $linestart, $want_space) = @_;
  my $is_tabbed = 1;
  my $tab_fields = $fields;
  $tab_fields =~ s/\(/(nowplaying|np|lastfm|lfm|/;
  if ($word =~ /(\%(?:$tab_fields))\(?(\w+)?\)?/) {
    my ($nowplaying, $user) = ($1, $3);
    undef $nowplaying if ($nowplaying =~ /nowplaying|np/);
    $nowplaying = lastfm_nowplaying($user, $is_tabbed, $nowplaying);
    if (lastfm_print(Irssi::active_win(), $nowplaying, 1)) {
      push @$complist, "$nowplaying";
    }
  }
};