Skip to content

Instantly share code, notes, and snippets.

@celediel
Last active March 12, 2016 19:54
Show Gist options
  • Save celediel/61020ad4b56898add81e to your computer and use it in GitHub Desktop.
Save celediel/61020ad4b56898add81e to your computer and use it in GitHub Desktop.
Cinch Last.fm Plugin

Last.fm Plugin for Cinch

A plugin to display certain information from last.fm in IRC.

Usage

Download the .rb file, and place it in a subdirectory of your bot entitled plugins then require it via require_relative. Add it to your bot like so:

require 'cinch'
require_relative 'plugins/lastfm.rb'

bot = Cinch::Bot.new do
configure do |c|
  c.server = 'your server'
  c.nick = 'your nick'
  c.realname = 'your realname'
  c.user = 'your user'
  c.channels = ['#yourchannel']
  c.plugins.plugins = [Cinch::Plugins::Lastfm]
  c.plugins.options[Cinch::Plugins::Lastfm] = { api_key: 'your-api-key' }
end

bot.start

Contained Commands

[fmset] username Sets your username and saves it in the config file.

[fmget] Display your set username.

[np] optional username Display last or currently playing track for you, or optional username.

[topartists] optional period -user optional username Display your top artists of period, or those of optional username. Period may be: 7d, 3m, 6m, 12m, 1y, or blank for overall.

[topalbums] optional period -user optional username Displays top albums of period Same syntax as topartists

[toptracks] optional period -user optional username Displays top tracks of period Same syntax as topartists

[profile] optional username Display some information about your last.fm account, or that of optional username.

License

Licensed under Creative Commons by-nc-sa 4.0 http://creativecommons.org/licenses/by-nc-sa/4.0/

#!/usr/bin/env ruby
# encoding=utf-8
require 'yaml' # Saving/loading databases
require 'nokogiri' # Decoding XML/HTML from lastfm api
require 'cgi' # Escaping/Unescaping HTML entities
require 'time_diff' # Calculating difference in times
require 'httparty' # Better HTTP Opening
require 'addressable/uri'
require 'webrick/httputils' # Fix for unicode URLs
module Cinch
module Plugins
# Last.fm plugin
class Lastfm
include Cinch::Plugin
def initialize(*args)
super
@base_url = 'http://ws.audioscrobbler.com/2.0/?api_key='
@api_key = config[:api_key]
@api_url = "#{@base_url}#{@api_key}&method="
@database = 'lastfm.yml'
end
private
def get_user(nick)
db = YAML.load(File.open(@database, 'r'))
usernick = db[nick.to_sym]
usernick
end
# Remove stupid pointless tags
def stupid_tags(tagarray)
tagarray.delete('seen live') # Worst tag on all of last.fm
tagarray.delete('indie')
# Why can't I have a return statement without rubocop getting angry
tagarray
end
def header(title)
puts '=' * 10 + " #{title} " + '=' * 10
end
public
# And now onto the commands
match(/np(?: (.+))?/, method: :np)
def np(m, query = nil)
method = 'user.getRecentTracks'
user = query.nil? ? get_user(m.user.nick.to_sym) : get_user(query.strip)
user = query.nil? ? m.user.nick : query.strip unless user
ac = '&autocorrect=1'
request = "#{@api_url}#{method}&user=#{user}"
# Last tracks
begin
page = HTTParty.get(Addressable::URI.parse(request))
doc = Nokogiri::XML(page.body)
# puts "Fetching #{request}"
if doc.xpath('//lfm')[0]['status'] == 'failed'
puts request
puts doc
m.reply "Error: #{doc.xpath('//error')[0].text}"
return
end
nowplaying = doc.xpath('//track')[0]['nowplaying']
whenplayed = doc.xpath('//track//date')[0]['uts'].to_i unless nowplaying
artist = doc.xpath('//track//artist')[0].text
title = doc.xpath('//track//name')[0].text
rescue NoMethodError
m.reply('No tracks found or username doesn\'t exist!')
return
end
begin
album = doc.xpath('//track//album')[0].text
puts "album = #{album}"
rescue NoMethodError
album = nil
end
album = nil if album == ''
# Track info
begin
header 'TRACK INFO'
doop = WEBrick::HTTPUtils.escape_form(artist)
pood = WEBrick::HTTPUtils.escape_form(title)
# puts doop
trackreq = "#{@api_url}track.getInfo&artist=#{doop}&track=#{pood}&user=#{user}#{ac}"
puts "trackreq: #{trackreq}"
query = trackreq.force_encoding('binary')
puts "query: #{query}"
page = HTTParty.get(Addressable::URI.parse(query))
trackdoc = Nokogiri::XML(page.body)
# puts "Fetching #{query}"
if trackdoc.xpath('//lfm')[0]['status'] == 'failed'
begin
m.reply error_code(trackdoc.xpath('//error')['code'].to_i)
return
rescue TypeError
puts 'derp'
end
end
header 'trackdoc'
puts trackdoc
header 'end trackdoc'
# listeners = trackdoc.xpath('//track//listeners').text
# playcount = trackdoc.xpath('//track//playcount').text
userpc = trackdoc.xpath('//track//userplaycount').text
loved = trackdoc.xpath('//track//userloved').text.to_i
# trackinfo = true
userplayed = true
puts userpc
header 'END TRACK INFO'
# rescue OpenURI::HTTPError
# trackinfo = false
rescue NoMethodError
userplayed = false
end
# Artist info
begin
doop = WEBrick::HTTPUtils.escape_form(artist)
# puts doop
artistreq = "#{@api_url}artist.getInfo#{ac}&artist=#{doop}&user=#{user}"
query = artistreq.force_encoding('binary')
page = HTTParty.get(Addressable::URI.parse(query))
artistdoc = Nokogiri::XML(page.body)
# puts "Fetching #{query}"
if artistdoc.xpath('//lfm')[0]['status'] == 'failed'
m.reply error_code(artistdoc.xpath('//error')[0]['code'].to_i)
return
end
artistinfo = false
toptags = artistdoc.xpath('//tags//tag//name')
tags = []
toptags.each { |i| tags << i.text }
tags = stupid_tags(tags)
artistinfo = true unless tags.empty?
rescue NoMethodError
artistinfo = false
end
unless nowplaying
future = if whenplayed > Time.now.to_i
true
else
false
end
end
output = nowplaying ? '♫' : '♪'
output << " #{artist} :: #{title}"
output << " [#{album}]" if album
output << ' ♥' unless loved.zero?
unless nowplaying
output << ' :: in ' + Time.diff(Time.at(whenplayed), Time.now)[:diff].to_s if future
output << ' :: ' + Time.diff(Time.at(whenplayed), Time.now)[:diff].to_s + ' ago' unless future
end
# output << ' ::' if nowplaying
output << " :: #{userpc} plays" if userplayed && !userpc.empty?
# output << " #{playcount} by #{listeners} others." if trackinfo
if artistinfo
len = tags.length >= 3 ? 3 : tags.length
output << ' :: '
tags.first(len).each_with_index do |i, j|
output.concat j == len - 1 ? i.to_s : "#{i}, "
end
output << ' ::'
else
output << ' :: no tags ::'
end
m.reply output
end
match(/artist(?: (.+))?/, method: :artist_search)
match(/plays(?: (.+))?/, method: :artist_search)
def artist_search(m, query)
method = 'artist.getInfo'
specific_user = false
list = query.split
if list.include?('-user')
specific_user = true
user_index = list.index('-user')
band = if user_index.zero?
list[user_index + 2..-1]
else
list[0..user_index - 1] + list[user_index + 2..-1]
end
query = band.join ' '
puts query
user = list[user_index + 1]
user = get_user(user) unless get_user(user).nil?
else
user = get_user(m.user.nick.to_sym)
user = m.user.nick if user.nil? || user.empty?
end
artist = query.force_encoding('binary')
artist = WEBrick::HTTPUtils.escape_form(artist)
ac = '&autocorrect=1'
artistreq = "#{@api_url}#{method}#{ac}&artist=#{artist}&user=#{user}"
page = HTTParty.get(Addressable::URI.parse(artistreq))
doc = Nokogiri::XML(page.body)
puts "Fetching #{artistreq}"
begin
a = doc.xpath('//artist//name')[0].text
toptags = doc.xpath('//tags//tag//name')
listeners = doc.xpath('//stats//listeners').text
playcount = doc.xpath('//stats//playcount').text
rescue NoMethodError
m.reply('Artist not found!')
return
end
begin
userpc = doc.xpath('//stats//userplaycount').text
userpc = nil if userpc == ''
rescue NoMethodError
userpc = nil
end
tags = []
toptags.each { |i| tags << i.text }
tags = stupid_tags(tags)
len = tags.length >= 3 ? 3 : tags.length
notags = tags.empty? ? true : false
output = a.to_s
if notags
output << ' :: no tags'
else
output << ' :: '
tags.first(len).each_with_index do |i, j|
output.concat j == len - 1 ? i.to_s : "#{i}, "
end
end
output.concat userpc ? " :: #{userpc} plays" : ' :: '
output << " by #{user} :: " if specific_user && userpc
output << ' :: ' if userpc && !specific_user
output << "#{playcount} by #{listeners} others ::"
m.reply output
end
# Currently broken per Last.fm
match(/compare(?: (.+))?/, method: :compare)
def compare(m, user1, user2 = nil)
method = 'tasteometer.compare'
user2 = get_user(m.user.nick.to_sym) unless user2
creq = "#{@api_url}#{method}&type1=user&type2=user&value1=#{user1}&value2=&#{user2}"
puts creq
page = HTTParty.get(Addressable::URI.parse(creq))
doc = Nokogiri::XML(page.body)
if doc.xpath('//lfm')[0]['status'] == 'failed'
m.reply("#{doc.xpath('//error')[0]['code']}: #{doc.xpath('//error').text}")
# return
end
puts doc
end
# My probably lousy attempt at not reusing code
match(/topartists(:? (.+))?/, method: :top_artists)
def top_artists(m, p)
tops(m, 'user.getTopArtists', p, 'artists')
end
match(/toptracks(:? (.+))?/, method: :top_tracks)
def top_tracks(m, p)
tops(m, 'user.getTopTracks', p, 'tracks')
end
match(/topalbums(:? (.+))?/, method: :top_albums)
def top_albums(m, p)
tops(m, 'user.getTopAlbums', p, 'albums')
end
def tops(m, meth, period, type)
# method = 'user.getTopArtists'
method = meth
period = 'overall' if period.nil?
period.strip!
list = period.split
if list.include?('-user')
user_index = list.index('-user')
per = if user_index.zero?
list[user_index + 2..-1]
else
list[0..user_index - 1] + list[user_index + 2..-1]
end
period = per.join ' '
user = list[user_index + 1]
else
user = get_user(m.user.nick.to_sym)
user = m.user.nick if user.nil? || user.empty?
end
case period
when '7d', '7day'
period = '7day'
str = '7 days'
when '1m', '1month'
period = '1month'
str = '1 month'
when '3m', '3month'
period = '3month'
str = '3 months'
when '6m', 'month'
period = '6month'
str = '6 months'
when '1y', '1year', '12m', '12month'
period = '12month'
str = '1 year'
else
period = 'overall'
str = period
end
urlget = "#{@api_url}#{method}&period=#{period}&user=#{user}&limit=10"
puts urlget
page = HTTParty.get(Addressable::URI.parse(urlget))
doc = Nokogiri::XML(page.body)
# puts "doc: #{doc.text}"
# This will only ever be true for 'overall'
output = if str == period
"#{str.capitalize} top #{type} "
else
"Top #{type} of the past #{str} "
end
# process the doc differently depending on query type (artists, tracks,
# albums, tags, etc.) There's probably a less shitty way to do this but
# oh well, I tried to not be redundant a bunch
if method == 'user.getTopArtists'
artists = []
plays_in_period = []
doc.xpath('//topartists//name').each { |i| artists << i.text }
doc.xpath('//topartists//playcount').each { |i| plays_in_period << i.text }
# Combine the two arrays into a single hash.
artplays = Hash[artists.zip(plays_in_period)]
puts "Artists: #{artists}: #{artists.class}"
puts "Artplays: #{artplays}: #{artplays.class}"
len = artplays.length >= 5 ? 5 : artplays.length
artplays.first(len).each_with_index do |(name, plays), i|
puts "name: #{name} :: plays: #{plays}, i: #{i}"
output << ":: #{name} (#{plays})"
output.concat i == len - 1 ? ' ::' : ' '
end
elsif method == 'user.getTopTracks'
artists = []
tracks = []
track_plays = []
doc.xpath('//artist//name').each { |i| artists << i.text }
doc.xpath('/lfm/toptracks/track/name').each { |i| tracks << i.text }
doc.xpath('//track//playcount').each { |i| track_plays << i.text }
puts "Artists: #{artists.length}, #{artists}"
puts "Tracks: #{tracks.length}, #{tracks}"
puts "Playcount: #{track_plays.length}, #{track_plays}"
unless artists.length == tracks.length && track_plays.length == tracks.length
# Something went horribly wrong, abort!
m.reply('Fission mailed!')
return
end
len = tracks.length >= 5 ? 5 : tracks.length
len.times do |i|
output << ":: #{artists[i]} - #{tracks[i]} (#{track_plays[i]})"
output.concat i == len - 1 ? ' ::' : ' '
end
elsif method == 'user.getTopAlbums'
artists = []
albums = []
album_plays = []
doc.xpath('//artist//name').each { |i| artists << i.text }
doc.xpath('/lfm/topalbums/album/name').each { |i| albums << i.text }
doc.xpath('//album//playcount').each { |i| album_plays << i.text }
puts "Artists: #{artists.length}, #{artists}"
puts "Albums: #{albums.length}, #{albums}"
puts "Playcount: #{album_plays.length}, #{album_plays}"
unless artists.length == albums.length && album_plays.length == albums.length
# Something went horribly wrong, abort!
m.reply('Fission mailed!')
return
end
len = albums.length >= 5 ? 5 : albums.length
len.times do |i|
output << ":: #{artists[i]} - #{albums[i]} (#{album_plays[i]})"
output.concat i == len - 1 ? ' ::' : ' '
end
end
m.reply output
end
# This doesn't seem to work either. Thanks last.fm
match(/toptags(:? (.+))?/, method: :top_tags)
def top_tags(m, u)
user = u.nil? ? get_user(m.user.nick.to_sym) : get_user(u.strip)
user = u.nil? ? m.user.nick : u.strip unless user
method = 'user.getTopTags'
urlget = "#{@api_url}#{method}&user=#{user}&limit=10"
puts urlget
page = HTTParty.get(Addressable::URI.parse(urlget))
doc = Nokogiri::XML(page.body)
tags = []
counts = []
doc.xpath('//name').each { |i| tags << i }
doc.xpath('//count').each { |i| counts << i }
output = "#{user}'s most used tags "
tagusage = Hash[tags.zip(counts)]
len = tagusage.length >= 5 ? 5 : tagusage.length
tagusage.first(len).each_with_index do |(name, plays), i|
puts "name: #{name} :: plays: #{plays}, i: #{i}"
output << ":: #{name} (#{plays})"
output.concat i == len - 1 ? ' ::' : ' '
end
output = 'User has not tagged anything!' if tags.empty? || counts.empty?
m.reply output
end
match(/tag(:? (.+))?/, method: :tag_search)
def tag_search(m, query)
method = 'tag.getInfo'
urlget = "#{@api_url}#{method}&tag=#{query.strip}"
puts urlget
page = HTTParty.get(Addressable::URI.parse(urlget))
doc = Nokogiri::XML(page.body)
num_used = doc.xpath('//tag//taggings').text
name = doc.xpath('//tag//name').text
summary = doc.xpath('//tag//wiki//content').text
output = "#{name} :: Used: #{num_used} times :: #{summary[0..130]}"
m.reply output
end
match(/profile(:? (.+))?/, method: :profile)
def profile(m, query)
method = 'user.getInfo'
user = query.nil? ? get_user(m.user.nick.to_sym) : get_user(query.strip)
user = query.nil? ? m.user.nick : query.strip unless user
# Fetch the data
url = "#{@api_url}#{method}&user=#{user}"
page = HTTParty.get(Addressable::URI.parse(url))
doc = Nokogiri::XML(page.body)
realname = doc.xpath('//realname').text
realname = 'No name' if realname.empty?
gender = doc.xpath('//gender').text
playcount = doc.xpath('//playcount').text
userurl = doc.xpath('//url').text
country = doc.xpath('//country').text
country = 'Nowhere' if country.empty?
reg = doc.xpath('//registered')[0]['unixtime']
regdate = DateTime.strptime(reg, '%s')
output = "#{user} :: #{realname} / #{gender} / #{country}"
output << " :: Plays: #{playcount}"
output << " :: Registered on: #{regdate.day} #{regdate.strftime('%b')} #{regdate.year}"
output << " :: #{userurl}"
m.reply(output)
end
match(/fmset(?: (.+))?/, method: :fmset)
def fmset(m, username = nil)
m.reply 'You need to input a username to set your username' && return if username.nil?
username = username.strip
# puts @database
# puts @database.class
if File.file?(@database)
f = File.open(@database, 'r')
db = YAML.load(f)
else
db = {}
end
f = File.open(@database, 'w+')
db.store(m.user.nick.to_sym, username)
f.write(db.to_yaml)
f.close
m.reply "#{m.user.nick} is now #{username}!"
end
match(/fmget(?: (.+))?/, method: :fmget)
def fmget(m)
user = get_user(m.user.nick.to_sym)
m.reply user ? "#{m.user.nick} is #{user}!" : 'I don\'t know your username!'
end
end
end
end
# vim:tabstop=2 softtabstop=2 expandtab shiftwidth=2 smarttab foldmethod=syntax:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment