]> git.sesse.net Git - vlc/blob - share/lua/extensions/imdb.lua
imdb.lua: Redesign, fix some bugs
[vlc] / share / lua / extensions / imdb.lua
1 --[[
2  Get information about a movie from IMDb
3
4  Copyright © 2009-2010 VideoLAN and AUTHORS
5
6  Authors:  Jean-Philippe André (jpeg@videolan.org)
7
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12
13  This program is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  GNU General Public License for more details.
17
18  You should have received a copy of the GNU General Public License
19  along with this program; if not, write to the Free Software
20  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
21 --]]
22
23 -- TODO: Use simplexml module to simplify parsing
24
25 -- Global variables
26 url = nil          -- string
27 title = nil        -- string
28 titles = {}        -- table, see code below
29
30 -- Some global variables: widgets
31 dlg = nil          -- dialog
32 txt = nil          -- text field
33 list = nil         -- list widget
34 button_open = nil  -- button widget
35 html = nil         -- rich text (HTML) widget
36 waitlbl = nil      -- text label widget
37
38 -- Script descriptor, called when the extensions are scanned
39 function descriptor()
40     return { title = "IMDb - The Internet Movie Database" ;
41              version = "1.0" ;
42              author = "Jean-Philippe André" ;
43              url = 'http://www.imdb.org/';
44              shortdesc = "The Internet Movie Database";
45              description = "<center><b>The Internet Movie Database</b></center><br />"
46                         .. "Get information about movies from the Internet "
47                         .. "Movie Database (IMDb).<br />This Extension will show "
48                         .. "you the cast, a short plot summary and a link to "
49                         .. "the web page on imdb.org." ;
50              capabilities = { "input-listener" } }
51 end
52
53 -- Remove trailing & leading spaces
54 function trim(str)
55     if not str then return "" end
56     return string.gsub(str, "^%s*(.*)+%s$", "%1")
57 end
58
59 -- Update title text field. Removes file extensions.
60 function update_title()
61     local item = vlc.input.item()
62     local name = item and item:name()
63     if name ~= nil then
64         name = string.gsub(name, "(.*)(%.%w+)$", "%1")
65     end
66     if name ~= nil then
67         txt:set_text(trim(name))
68     end
69 end
70
71 -- Function called when the input (media being read) changes
72 function input_changed()
73     update_title()
74 end
75
76 -- First function to be called when the extension is activated
77 function activate()
78     create_dialog()
79 end
80
81 -- This function is called when the extension is disabled
82 function deactivate()
83 end
84
85 -- Create the main dialog with a simple search bar
86 function create_dialog()
87     dlg = vlc.dialog("IMDb")
88     dlg:add_label("<b>Movie Title:</b>", 1, 1, 1, 1)
89     local item = vlc.input.item()
90     txt = dlg:add_text_input(item and item:name() or "", 2, 1, 1, 1)
91     dlg:add_button("Search", click_okay, 3, 1, 1, 1)
92     -- Show, if not already visible
93     dlg:show()
94 end
95
96 -- Dialog closed
97 function close()
98     -- Deactivate this extension
99     vlc.deactivate()
100 end
101
102 -- Called when the user presses the "Search" button
103 function click_okay()
104     vlc.msg.dbg("[IMDb] Searching for " .. txt:get_text())
105
106     -- Search IMDb: build URL
107     title = string.gsub(string.gsub(txt:get_text(), "[%p%s%c]", "+"), "%++", " ")
108     url = "http://www.imdb.com/find?s=all&q=" .. string.gsub(title, " ", "+")
109
110     -- Recreate dialog structure: delete useless widgets
111     if html then
112         dlg:del_widget(html)
113         html = nil
114     end
115
116     if list then
117         dlg:del_widget(list)
118         dlg:del_widget(button_open)
119         list = nil
120         button_open = nil
121     end
122
123     -- Ask the user to wait some time...
124     local waitmsg = 'Searching for <a href="' .. url .. '">' .. title .. "</a> on IMDb..."
125     if not waitlbl then
126         waitlbl = dlg:add_label(waitmsg, 1, 2, 3, 1)
127     else
128         waitlbl:set_text(waitmsg)
129     end
130     dlg:update()
131
132     -- Download the data
133     local s, msg = vlc.stream(url)
134     if not s then
135         vlc.msg.warn("[IMDb] " .. msg)
136         waitlbl:set_text('Sorry, an error occured while searching for <a href="'
137                          .. url .. '">' .. title .. "</a>.<br />Please try again later.")
138         return
139     end
140
141     -- Fetch HTML data
142     local data = s:read(65000)
143     if not data then
144         vlc.msg.warn("[IMDb] Not data received!")
145         waitlbl:set_text('Sorry, an error occured while searching for <a href="'
146                          .. url .. '">' .. title .. "</a>.<br />Please try again later.")
147         return
148     end
149
150     -- Probe result & parse it
151     if string.find(data, "<h6>Overview</h6>") then
152         -- We found a direct match
153         parse_moviepage(data)
154     else
155         -- We have a list of results to parse
156         parse_resultspage(data)
157     end
158 end
159
160 -- Called when clicked on the "Open" button
161 function click_open()
162     -- Get user selection
163     selection = list:get_selection()
164     if not selection then return end
165
166     local sel = nil
167     for idx, selectedItem in pairs(selection) do
168         sel = idx
169         break
170     end
171     if not sel then return end
172     local imdbID = titles[sel].id
173
174     -- Update information message
175     url = "http://www.imdb.org/title/" .. imdbID .. "/"
176     title = titles[sel].title
177
178     dlg:del_widget(list)
179     dlg:del_widget(button_open)
180     list = nil
181     button_open = nil
182     waitlbl:set_text("Loading IMDb page for <a href=\"" .. url .. "\">" .. title .. "</a>.")
183     dlg:update()
184
185     local s, msg = vlc.stream(url)
186     if not s then
187         waitlbl:set_text('Sorry, an error occured while looking for <a href="'
188                          .. url .. '">' .. title .. "</a>.")
189         vlc.msg.warn("[IMDb] " .. msg)
190         return
191     end
192
193     data = s:read(65000)
194     if data and string.find(data, "<h6>Overview</h6>") then
195         parse_moviepage(data)
196     else
197         waitlbl:set_text('Sorry, no results found for <a href="'
198                          .. url .. '">' .. title .. "</a>.")
199     end
200 end
201
202 -- Parse the results page and find titles, years & URL's
203 function parse_resultspage(data)
204     vlc.msg.dbg("[IMDb] Analysing results page")
205
206     -- Find titles
207     titles = {}
208     local count = 0
209
210     local idxEnd = 1
211     while idxEnd ~= nil do
212         -- Find title types
213         local titleType = nil
214         _, idxEnd, titleType = string.find(data, "<b>([^<]*Titles[^<]*)</b>", idxEnd)
215         local _, _, nextTitle = string.find(data, "<b>([^<]*Titles[^<]*)</b>", idxEnd)
216         if not titleType then
217             break
218         else
219             -- Find current scope
220             local table = nil
221             if not nextTitle then
222                 _, _, table = string.find(data, "<table>(.*)</table>", idxEnd)
223             else
224                 nextTitle = string.gsub(nextTitle, "%(", "%%(")
225                 nextTitle = string.gsub(nextTitle, "%)", "%%)")
226                 _, _, table = string.find(data, "<table>(.*)</table>.*"..nextTitle, idxEnd)
227             end
228
229             if not table then break end
230             local pos = 0
231             local thistitle = nil
232
233             -- Find all titles in this scope
234             while pos ~= nil do
235                 local _, _, link = string.find(table, "<a href=\"([^\"]+title[^\"]+)\"", pos)
236                 if not link then break end -- this would not be normal behavior...
237                 _, pos, thistitle = string.find(table, "<a href=\"" .. link .. "\"[^>]*>([^<]+)</a>", pos)
238                 if not thistitle then break end -- this would not be normal behavior...
239                 local _, _, year = string.find(table, "\((%d+)\)", pos)
240                 -- Add this title to the list
241                 count = count + 1
242                 local _, _, imdbID = string.find(link, "/([^/]+)/$")
243                 thistitle = replace_html_chars(thistitle)
244                 titles[count] = { id = imdbID ; title = thistitle ; year = year ; link = link }
245             end
246         end
247     end
248
249     -- Did we find anything at all?
250     if not count or count == 0 then
251         waitlbl:set_text('Sorry, no results found for <a href="'
252                          .. url .. '">' .. title .. "</a>.")
253         return
254     end
255
256     -- Sounds good, we found some results, let's display them
257     waitlbl:set_text(count .. " results found for <a href=\"" .. url .. "\">" .. title .. "</a>.")
258     list = dlg:add_list(1, 3, 3, 1)
259     button_open = dlg:add_button("Open", click_open, 3, 4, 1, 1)
260
261     for idx, title in ipairs(titles) do
262         --list:add_value("[" .. title.id .. "] " .. title.title .. " (" .. title.year .. ")", idx)
263         list:add_value(title.title .. " (" .. title.year .. ")", idx)
264     end
265 end
266
267 -- Parse a movie description page
268 function parse_moviepage(data)
269     -- Title & year
270     title = string.gsub(data, "^.*<title>(.*)</title>.*$", "%1")
271     local text = "<h1>" .. title .. "</h1>"
272     text = text .. "<h2>Overview</h2><table>"
273
274     -- Real URL
275     url = string.gsub(data, "^.*<link rel=\"canonical\" href=\"([^\"]+)\".*$", "%1")
276     local imdbID = string.gsub(url, "^.*/title/([^/]+)/.*$", "%1")
277     if imdbID then
278         url = "http://www.imdb.org/title/" .. imdbID .. "/"
279     end
280
281     -- Director
282     local director = nil
283     _, nextIdx, _ = string.find(data, "<div id=\"director-info\"", 1, true)
284     if nextIdx then
285         _, _, director = string.find(data, "<a href[^>]+>([%w%s]+)</a>", nextIdx)
286     end
287     if not director then
288         director = "(Unknown)"
289     end
290     text = text .. "<tr><td><b>Director</b></td><td>" .. director .. "</td></tr>"
291
292     -- Main genres
293     local genres = "<tr><td><b>Genres</b></td>"
294     local first = true
295     for genre, _ in string.gmatch(data, "/Sections/Genres/(%w+)/\">") do
296         if first then
297             genres = genres .. "<td>" .. genre .. "</td></tr>"
298         else
299             genres = genres .. "<tr><td /><td>" .. genre .. "</td></tr>"
300         end
301         first = false
302     end
303     text = text .. genres
304
305     -- List main actors
306     local actors = "<tr><td><b>Cast</b></td>"
307     local first = true
308     for nm, char in string.gmatch(data, "<td class=\"nm\"><a[^>]+>([%w%s]+)</a></td><td class=\"ddd\"> ... </td><td class=\"char\"><a[^>]+>([%w%s]+)</a>") do
309         if not first then
310             actors = actors .. "<tr><td />"
311         end
312         actors = actors .. "<td>" .. nm .. "</td><td><i>" .. char .. "</i></td></tr>"
313         first = false
314     end
315     text = text .. actors .. "</table>"
316
317     waitlbl:set_text("<center><a href=\"" .. url .. "\">" .. title .. "</a></center>")
318     if list then
319         dlg:del_widget(list)
320         dlg:del_widget(button_open)
321     end
322     html = dlg:add_html(text .. "<br />Loading summary...", 1, 3, 3, 1)
323     dlg:update()
324
325     text = text .. "<h2>Plot Summary</h2>"
326     local s, msg = vlc.stream(url .. "plotsummary")
327     if not s then
328         vlc.msg.warn("[IMDb] " .. msg)
329         return
330     end
331     local data = s:read(65000)
332
333     -- We read only the first summary
334     _, _, summary = string.find(data, "<p class=\"plotpar\">([^<]+)")
335     if not summary then
336         summary = "(Unknown)"
337     end
338     text = text .. "<p>" .. summary .. "</p>"
339     text = text .. "<p><h2>Source IMDb</h2><a href=\"" .. url .. "\">" .. url .. "</a></p>"
340
341     html:set_text(text)
342 end
343
344 -- Convert some HTML characters into UTF8
345 function replace_html_chars(txt)
346     if not txt then return nil end
347     -- return vlc.strings.resolve_xml_special_chars(txt)
348     for num in string.gmatch(txt, "&#x(%x+);") do
349         -- Convert to decimal (any better way?)
350         dec = 0
351         for c in string.gmatch(num, "%x") do
352             cc = string.byte(c) - string.byte("0")
353             if (cc >= 10 or cc < 0) then
354                 cc = string.byte(string.lower(c)) - string.byte("a") + 10
355             end
356             dec = dec * 16 + cc
357         end
358         txt = string.gsub(txt, "&#x" .. num .. ";", string.char(dec))
359     end
360     return txt
361 end
362