]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/imdb.rb
imdb plugin: some titles with extra info where missed when searching for movies by...
[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)
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
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       ratings = "no votes"
160       m = /<b>([0-9.]+)\/10<\/b>\n?\r?\s+<small>\(<a href="ratings">([0-9,]+) votes?<\/a>\)<\/small>/.match(resp.body)
161       if m
162         ratings = "#{m[1]}/10 (#{m[2]} voters)"
163       end
164
165       genre = Array.new
166       resp.body.scan(/<a href="\/Sections\/Genres\/[^\/]+\/">([^<]+)<\/a>/) do |gnr|
167         genre << gnr
168       end
169
170       plot = nil
171       data = grab_info(/Plot (?:Outline|Summary)/, resp.body)
172       if data
173         plot = "Plot: " + data.ircify_html.gsub(/\s+more$/,'')
174       end
175
176       info << ["Ratings: " << ratings, "Genre: " << genre.join('/') , plot].compact.join(". ")
177
178       return info
179     end
180     return nil
181   end
182
183   def info_name(sr, opts={})
184     resp = nil
185     begin
186       resp = @bot.httputil.get_response(IMDB + sr, :max_redir => -1)
187     rescue Exception => e
188       error e.message
189       warning e.backtrace.join("\n")
190       return nil
191     end
192
193     info = []
194
195     if resp.code == "200"
196       m = /<title>([^<]*)<\/title>/.match(resp.body)
197       return nil if !m
198       name = m[1]
199
200       info << "#{name} : http://us.imdb.com#{sr}"
201
202       if year = opts[:movies_in_year]
203         filmoyear = @bot.httputil.get(IMDB + sr + "filmoyear")
204         if filmoyear
205           info << filmoyear.scan(/#{TITLE_MATCH} \(#{year}\)[^\]]*\[(.*)\]([^<]+)?(?:$|\s*<)/)
206         end
207         return info
208       end
209
210       birth = nil
211       data = grab_info("Date of Birth", resp.body)
212       if data
213         birth = "Birth: #{data.ircify_html.gsub(/\s+more$/,'')}"
214       end
215
216       death = nil
217       data = grab_info("Date of Death", resp.body)
218       if data
219         death = "Death: #{data.ircify_html.gsub(/\s+more$/,'')}"
220       end
221
222       info << [birth, death].compact.join('. ') if birth or death
223
224       movies = {}
225
226       filmorate = nil
227       begin
228         filmorate = @bot.httputil.get(IMDB + sr + "filmorate")
229       rescue Exception
230       end
231
232       if filmorate
233         filmorate.scan(/<div class="filmo">.*?<a href="\/title.*?<\/div>/m) { |str|
234           what = str.match(/<a name="[^"]+">([^<]+)<\/a>/)[1] rescue nil
235           next unless what
236           movies[what] = str.scan(TITLE_MATCH)[0..2].map { |url, tit|
237             fix_article(tit.ircify_html)
238           }
239         }
240       end
241
242       preferred = ['Actor', 'Director']
243       if resp.body.match(/Jump to filmography as:&nbsp;(.*?)<\/div>/)
244         txt = $1
245         preferred = txt.scan(/<a[^>]+>([^<]+)<\/a>/)[0..2].map { |pref|
246           pref.first
247         }
248       end
249
250       unless movies.empty?
251         all_keys = movies.keys.sort
252         debug all_keys.inspect
253         keys = []
254         preferred.each { |key|
255           keys << key if all_keys.include? key
256         }
257         keys = all_keys if keys.empty?
258         ar = []
259         keys.each { |key|
260           ar << key.dup
261           ar.last << ": " + movies[key].join('; ')
262         }
263         info << ar.join('. ')
264       end
265       return info
266
267     end
268     return nil
269   end
270
271   def year_movies(urls, year)
272     urls.map { |url|
273       info = info_name(url, :movies_in_year => year)
274
275       name_url = info.first
276       data = info[1]
277
278       movies = []
279       data.each { |url, pre_title, pre_roles, extra|
280         title = fix_article(pre_title.ircify_html)
281         role_array = pre_roles.split(/\]\s+\[/).map { |txt|
282           if txt.match(/^(.*)\s+\.\.\.\.\s+(.*)$/)
283             "#{$1} (#{$2})"
284           else
285             txt
286           end
287         }
288         role_array.last << " " + extra.ircify_html if extra
289
290         roles = role_array.join(', ')
291         movies << [roles, title].join(": ")
292       }
293
294       if movies.empty?
295         [name_url, nil]
296       else
297         [name_url, movies.join(" | ")]
298       end
299     }
300   end
301
302 end
303
304 class ImdbPlugin < Plugin
305   BotConfig.register BotConfigBooleanValue.new('imdb.aka',
306     :default => true,
307     :desc => "Look for IMDB matches also in translated titles and other 'also known as' information")
308   BotConfig.register BotConfigBooleanValue.new('imdb.popular',
309     :default => true,
310     :desc => "Display info on popular IMDB entries matching the request closely")
311   BotConfig.register BotConfigBooleanValue.new('imdb.exact',
312     :default => true,
313     :desc => "Display info on IMDB entries matching the request exactly")
314   BotConfig.register BotConfigBooleanValue.new('imdb.fix_article',
315     :default => false,
316     :desc => "Try to detect an article placed at the end and move it in front of the title")
317
318   def help(plugin, topic="")
319     "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"
320   end
321
322   attr_reader :i
323
324   def initialize
325     super
326     @i = Imdb.new(@bot)
327   end
328
329   def imdb(m, params)
330     what = params[:what].to_s
331     type = params[:type].intern
332     info = i.info(what, :type => type)
333     if !info
334       m.reply "Nothing found for #{what}"
335       return nil
336     end
337     if info.length == 1
338       m.reply Utils.decode_html_entities info.first.join("\n")
339     else
340       m.reply info.map { |si|
341         Utils.decode_html_entities si.join(" | ")
342       }.join("\n")
343     end
344   end
345
346   def movies(m, params)
347     who = params[:who].to_s
348     year = params[:year]
349
350     name_urls = i.search(who, :type => :name)
351     unless name_urls
352       m.reply "nothing found about #{who}, sorry"
353       return
354     end
355
356     movie_urls = i.year_movies(name_urls, year)
357     debug movie_urls.inspect
358     debug movie_urls[0][1]
359
360     if movie_urls.length == 1 and movie_urls[0][1]
361       m.reply movie_urls.join("\n")
362     else
363       m.reply movie_urls.map { |si|
364         si[1] = "no movies in #{year}" unless si[1]
365         Utils.decode_html_entities si.join(" | ")
366       }.join("\n")
367     end
368   end
369
370 end
371
372 plugin = ImdbPlugin.new
373 plugin.map "movies :prefix *who in :year", :requirements => { :prefix => /with|by|from/, :year => /\d+/ }
374 plugin.map "imdb [:type] *what", :requirements => { :type => /name|title/ }, :defaults => { :type => 'both' }
375