]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/imdb.rb
imdb plugin: character lookup by actor and movie
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / imdb.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: IMDB plugin for rbot
5 #
6 # Author:: Arnaud Cornet <arnaud.cornet@gmail.com>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2005 Arnaud Cornet
10 # Copyright:: (C) 2007 Giuseppe Bilotta
11 #
12 # License:: MIT license
13
14 require 'uri/common'
15
16 class Imdb
17   IMDB = "http://us.imdb.com"
18   TITLE_OR_NAME_MATCH = /<a href="(\/(?:title|name)\/(?:tt|nm)[0-9]+\/?)[^"]*"(?:[^>]*)>([^<]*)<\/a>/
19   TITLE_MATCH = /<a href="(\/title\/tt[0-9]+\/?)[^"]*"(?:[^>]*)>([^<]*)<\/a>/
20   NAME_MATCH = /<a href="(\/name\/nm[0-9]+\/?)[^"]*"(?:[^>]*)>([^<]*)<\/a>/
21   FINAL_ARTICLE_MATCH = /, ([A-Z]\S{0,2})$/
22
23   MATCHER = {
24     :title => TITLE_MATCH,
25     :name => NAME_MATCH,
26     :both => TITLE_OR_NAME_MATCH
27   }
28
29   def initialize(bot)
30     @bot = bot
31   end
32
33   def search(rawstr, rawopts={})
34     str = URI.escape(rawstr)
35     str << ";site=aka" if @bot.config['imdb.aka']
36     opts = rawopts.dup
37     opts[:type] = :both unless opts[:type]
38     return do_search(str, opts)
39   end
40
41   def do_search(str, opts={})
42     resp = nil
43     begin
44       resp = @bot.httputil.get_response(IMDB + "/find?q=#{str}",
45                                         :max_redir => -1)
46     rescue Exception => e
47       error e.message
48       warning e.backtrace.join("\n")
49       return nil
50     end
51
52
53     matcher = MATCHER[opts[:type]]
54
55     if resp.code == "200"
56       m = []
57       m << matcher.match(resp.body) if @bot.config['imdb.popular']
58       if resp.body.match(/\(Exact Matches\)<\/b>/) and @bot.config['imdb.exact']
59         m << matcher.match($')
60       end
61       m.compact!
62       unless m.empty?
63         return m.map { |mm|
64           mm[1]
65         }.uniq
66       end
67     elsif resp.code == "302"
68       debug "automatic redirection"
69       new_loc = resp['location'].gsub(IMDB, "")
70       if new_loc.match(/\/find\?q=(.*)/)
71         return do_search($1, opts)
72       else
73         return [new_loc.gsub(/\?.*/, "")]
74       end
75     end
76     return nil
77   end
78
79   def info(rawstr, opts={})
80     debug opts.inspect
81     urls = search(rawstr, opts)
82     debug urls
83     if urls.nil_or_empty?
84       debug "IMDB: search returned NIL"
85       return nil
86     end
87     results = []
88     urls.each { |sr|
89       type = sr.match(/^\/([^\/]+)\//)[1].downcase.intern rescue nil
90       case type
91       when :title
92         results << info_title(sr)
93       when :name
94         results << info_name(sr)
95       else
96         results << "#{sr}"
97       end
98     }
99     return results
100   end
101
102   def grab_info(info, body)
103     /<div class="info">\s+<h5>#{info}:<\/h5>\s+(.*?)<\/div>/mi.match(body)[1] rescue nil
104   end
105
106   def fix_article(org_tit)
107     title = org_tit.dup
108     debug title.inspect
109     if title.match(/^"(.*)"$/)
110       return "\"#{fix_article($1)}\""
111     end
112     if @bot.config['imdb.fix_article'] and title.gsub!(FINAL_ARTICLE_MATCH, '')
113       art = $1.dup
114       debug art.inspect
115       if art[-1,1].match(/[A-Za-z]/)
116         art << " "
117       end
118       return art + title
119     end
120     return title
121   end
122
123   def info_title(sr, opts={})
124     resp = nil
125     begin
126       resp = @bot.httputil.get_response(IMDB + sr, :max_redir => -1)
127     rescue Exception => e
128       error e.message
129       warning e.backtrace.join("\n")
130       return nil
131     end
132
133     info = []
134
135     if resp.code == "200"
136       m = /<title>([^<]*)<\/title>/.match(resp.body)
137       return nil if !m
138       title_date = m[1]
139       pre_title, date, extra = title_date.scan(/^(.*)\((\d\d\d\d(?:\/[IV]+)?)\)\s*(.+)?$/).first
140       pre_title.strip!
141       title = fix_article(pre_title.ircify_html)
142
143       dir = nil
144       data = grab_info(/Directors?/, resp.body)
145       if data
146         dir = data.scan(NAME_MATCH).map { |url, name|
147           name.ircify_html
148         }.join(', ')
149       end
150
151       country = nil
152       data = grab_info(/Country/, resp.body)
153       if data
154         country = data.ircify_html.gsub(' / ','/')
155       end
156
157       info << [title, "(#{country}, #{date})", extra, dir ? "[#{dir}]" : nil, ": http://us.imdb.com#{sr}"].compact.join(" ")
158
159       return info if opts[:title_only]
160
161       ratings = "no votes"
162       m = /<b>([0-9.]+)\/10<\/b>\n?\r?\s+<small>\(<a href="ratings">([0-9,]+) votes?<\/a>\)<\/small>/.match(resp.body)
163       if m
164         ratings = "#{m[1]}/10 (#{m[2]} voters)"
165       end
166
167       genre = Array.new
168       resp.body.scan(/<a href="\/Sections\/Genres\/[^\/]+\/">([^<]+)<\/a>/) do |gnr|
169         genre << gnr
170       end
171
172       plot = nil
173       data = grab_info(/Plot (?:Outline|Summary)/, resp.body)
174       if data
175         plot = "Plot: " + data.ircify_html.gsub(/\s+more$/,'')
176       end
177
178       info << ["Ratings: " << ratings, "Genre: " << genre.join('/') , plot].compact.join(". ")
179
180       return info
181     end
182     return nil
183   end
184
185   def info_name(sr, opts={})
186     resp = nil
187     begin
188       resp = @bot.httputil.get_response(IMDB + sr, :max_redir => -1)
189     rescue Exception => e
190       error e.message
191       warning e.backtrace.join("\n")
192       return nil
193     end
194
195     info = []
196
197     if resp.code == "200"
198       m = /<title>([^<]*)<\/title>/.match(resp.body)
199       return nil if !m
200       name = m[1]
201
202       info << "#{name} : http://us.imdb.com#{sr}"
203
204       return info if opts[:name_only]
205
206       if year = opts[:movies_in_year]
207         filmoyear = @bot.httputil.get(IMDB + sr + "filmoyear")
208         if filmoyear
209           info << filmoyear.scan(/#{TITLE_MATCH} \(#{year}\)[^\[\n]*((?:\s+\[[^\]]+\](?:\s+\([^\[<]+\))*)+)\s+</)
210         end
211         return info
212       end
213
214       birth = nil
215       data = grab_info("Date of Birth", resp.body)
216       if data
217         birth = "Birth: #{data.ircify_html.gsub(/\s+more$/,'')}"
218       end
219
220       death = nil
221       data = grab_info("Date of Death", resp.body)
222       if data
223         death = "Death: #{data.ircify_html.gsub(/\s+more$/,'')}"
224       end
225
226       info << [birth, death].compact.join('. ') if birth or death
227
228       movies = {}
229
230       filmorate = nil
231       begin
232         filmorate = @bot.httputil.get(IMDB + sr + "filmorate")
233       rescue Exception
234       end
235
236       if filmorate
237         filmorate.scan(/<div class="filmo">.*?<a href="\/title.*?<\/div>/m) { |str|
238           what = str.match(/<a name="[^"]+">([^<]+)<\/a>/)[1] rescue nil
239           next unless what
240           movies[what] = str.scan(TITLE_MATCH)[0..2].map { |url, tit|
241             fix_article(tit.ircify_html)
242           }
243         }
244       end
245
246       preferred = ['Actor', 'Director']
247       if resp.body.match(/Jump to filmography as:&nbsp;(.*?)<\/div>/)
248         txt = $1
249         preferred = txt.scan(/<a[^>]+>([^<]+)<\/a>/)[0..2].map { |pref|
250           pref.first
251         }
252       end
253
254       unless movies.empty?
255         all_keys = movies.keys.sort
256         debug all_keys.inspect
257         keys = []
258         preferred.each { |key|
259           keys << key if all_keys.include? key
260         }
261         keys = all_keys if keys.empty?
262         ar = []
263         keys.each { |key|
264           ar << key.dup
265           ar.last << ": " + movies[key].join('; ')
266         }
267         info << ar.join('. ')
268       end
269       return info
270
271     end
272     return nil
273   end
274
275   def year_movies(urls, year)
276     urls.map { |url|
277       info = info_name(url, :movies_in_year => year)
278
279       debug info.inspect
280
281       name_url = info.first
282       data = info[1]
283
284       movies = []
285       # Sort by pre-title putting movies before TV series
286       data.sort { |a, b|
287         aclip = a[1][0,5]
288         bclip = b[1][0,5]
289         quot = '&#34;'
290         (aclip == quot ? 1 : -1) <=> (bclip == quot ? 1 : -1)
291       }.each { |url, pre_title, pre_roles|
292         title = fix_article(pre_title.ircify_html)
293         if title[0] == ?" and not @bot.config['imdb.tv_series_in_movies']
294           next
295         end
296         role_array = []
297         pre_roles.strip.scan(/\[([^\]]+)\]((?:\s+\([^\[]+\))+)?/) { |txt, comm|
298           if txt.match(/^(.*)\s+\.\.\.\.\s+(.*)$/)
299             role_array << "#{$1} (#{$2})"
300           else
301             role_array << txt
302           end
303           role_array.last << " " + comm.ircify_html if comm
304         }
305
306         roles = role_array.join(', ')
307         movies << [roles, title].join(": ")
308       }
309
310       if movies.empty?
311         [name_url, nil]
312       else
313         [name_url, movies.join(" | ")]
314       end
315     }
316   end
317
318   def name_in_movie(name_urls, movie_urls)
319     info = []
320     movie_urls.each { |movie|
321       title_info = info_title(movie, :title_only => true)
322       valid = []
323
324       data = @bot.httputil.get(IMDB + movie + "fullcredits")
325       data.scan(/#{NAME_MATCH}<\/td><td[^>]+> \.\.\. <\/td><td[^>]+>(.+?)<\/td>/).each { |url, name, role|
326         valid << [url, name.ircify_html, role.ircify_html] if name_urls.include?(url)
327       }
328       valid.each { |url, name, role|
329         info << "%s : %s was %s in %s" % [name, IMDB + url, role, title_info]
330       }
331     }
332     return info
333   end
334
335
336 end
337
338 class ImdbPlugin < Plugin
339   BotConfig.register BotConfigBooleanValue.new('imdb.aka',
340     :default => true,
341     :desc => "Look for IMDB matches also in translated titles and other 'also known as' information")
342   BotConfig.register BotConfigBooleanValue.new('imdb.popular',
343     :default => true,
344     :desc => "Display info on popular IMDB entries matching the request closely")
345   BotConfig.register BotConfigBooleanValue.new('imdb.exact',
346     :default => true,
347     :desc => "Display info on IMDB entries matching the request exactly")
348   BotConfig.register BotConfigBooleanValue.new('imdb.fix_article',
349     :default => false,
350     :desc => "Try to detect an article placed at the end and move it in front of the title")
351   BotConfig.register BotConfigBooleanValue.new('imdb.tv_series_in_movies',
352     :default => false,
353     :desc => "Whether searching movies by person/year should also return TV series")
354
355   def help(plugin, topic="")
356     "imdb <string> => search http://www.imdb.org for <string>: prefix <string> with 'name' or 'title' if you only want to search for people or films respectively, e.g.: imdb name ed wood"
357   end
358
359   attr_reader :i
360
361   def initialize
362     super
363     @i = Imdb.new(@bot)
364   end
365
366   def imdb(m, params)
367     what = params[:what].to_s
368     type = params[:type].intern
369     info = i.info(what, :type => type)
370     if !info
371       m.reply "Nothing found for #{what}"
372       return nil
373     end
374     if info.length == 1
375       m.reply Utils.decode_html_entities info.first.join("\n")
376     else
377       m.reply info.map { |si|
378         Utils.decode_html_entities si.join(" | ")
379       }.join("\n")
380     end
381   end
382
383   def movies(m, params)
384     who = params[:who].to_s
385     year = params[:year]
386
387     name_urls = i.search(who, :type => :name)
388     unless name_urls
389       m.reply "nothing found about #{who}, sorry"
390       return
391     end
392
393     movie_urls = i.year_movies(name_urls, year)
394     debug movie_urls.inspect
395     debug movie_urls[0][1]
396
397     if movie_urls.length == 1 and movie_urls[0][1]
398       m.reply movie_urls.join("\n")
399     else
400       m.reply movie_urls.map { |si|
401         si[1] = "no movies in #{year}" unless si[1]
402         Utils.decode_html_entities si.join(" | ")
403       }.join("\n")
404     end
405   end
406
407   def character(m, params)
408     who = params[:who].to_s
409     movie = params[:movie].to_s
410
411     name_urls = i.search(who, :type => :name)
412     unless name_urls
413       m.reply "nothing found about #{who}, sorry"
414       return
415     end
416
417     movie_urls = i.search(movie, :type => :title)
418     unless movie_urls
419       m.reply "nothing found about #{hwo}, sorry"
420       return
421     end
422
423     info = i.name_in_movie(name_urls, movie_urls)
424     m.reply info.join("\n")
425   end
426
427 end
428
429 plugin = ImdbPlugin.new
430
431 plugin.map "imdb [:type] *what", :requirements => { :type => /name|title/ }, :defaults => { :type => 'both' }
432 plugin.map "movies :prefix *who in :year", :requirements => { :prefix => /with|by|from/, :year => /\d+/ }
433 plugin.map "character [played] by *who in *movie"
434 plugin.map "character of *who in *movie"
435