Ticket #6225: ofdb.py

File ofdb.py, 9.9 KB (added by mythtv@…, 18 years ago)
Line 
1#!/usr/bin/python
2# -*- coding: utf8 -*-
3
4# This script performs movie data lookup using the German www.ofdb.de
5# website.
6#
7# For more information on MythVideo's external movie lookup mechanism, see
8# the README file in this directory.
9#
10# This script is based on a PHP script contributed by Christian GÃŒdel and
11# the original ofdb.pl written by Xavier Hervy (maxpower44 AT tiscali DOT fr).
12#
13# Requirements:
14# PyXML
15
16import os
17import sys
18import re
19import urlparse
20import urllib
21import urllib2
22import cgi
23import traceback
24import codecs
25from optparse import OptionParser
26
27# This is done for Ubuntu, they are removing PyXML.
28alt_path = '/usr/lib/python%s/site-packages/oldxml' % sys.version[:3]
29if os.path.exists(alt_path):
30 sys.path.append(alt_path)
31
32from xml.dom.ext.reader import HtmlLib
33from xml.dom import EMPTY_NAMESPACE
34from xml import xpath
35
36# The Python SGML based HTML parser is unusable (it finds
37# tag start/ends in the worst way possible).
38
39VERBOSE=False
40URL_BASE="http://www.ofdb.de"
41DUMP_RESPONSE=False
42
43ofdb_version = "0.3"
44mythtv_version = "0.21"
45
46def comment_out(str):
47 s = str
48 try:
49 s = unicode(str, "utf8")
50 except:
51 pass
52
53 print("# %s" % (s,))
54
55def debug_out(str):
56 if VERBOSE:
57 comment_out(str)
58
59def response_out(str):
60 if DUMP_RESPONSE:
61 s = str
62 try:
63 s = unicode(str, "utf8")
64 except:
65 pass
66
67 print(s)
68
69def print_exception(str):
70 for line in str.splitlines():
71 comment_out(line)
72
73def _xmlprep(content):
74 """Removes any HTML tags that just confuse the parser."""
75
76 pat = re.compile(r'<\s*meta.*?>', re.M)
77 ret = pat.sub('', content)
78 pat = re.compile(r'<\s*script.*?<\s*/script\s*>', re.M | re.S)
79 return pat.sub('', ret)
80
81
82def _myth_url_get(url, data = None, as_post = False):
83 extras = ['ofdb', ofdb_version]
84
85 debug_out("_myth_url_get(%s, %s, %s)" % (url, data, as_post))
86 send_data = {}
87 if data:
88 send_data.update(data)
89 dest_url = url
90
91 if not as_post:
92 # TODO: cgi.parse_qs does not handle valueless entries
93 # so things like ?foo will fail, ?foo=val works.
94 (scheme, netloc, path, query, frag) = urlparse.urlsplit(dest_url)
95 send_data = {}
96
97 old_qa = cgi.parse_qs(query)
98 if old_qa:
99 send_data.update(old_qa)
100
101 if data:
102 send_data.update(data)
103
104 query = urllib.urlencode(send_data)
105 send_data = None
106 dest_url = urlparse.urlunsplit((scheme, netloc, path, query, frag))
107
108 req = urllib2.Request(url = dest_url, headers =
109 { 'User-Agent' : "MythTV/%s (%s)" %
110 (mythtv_version, "; ".join(extras))})
111
112 if send_data:
113 req.add_data(urllib.urlencode(send_data))
114
115 try:
116 debug_out("Get URL '%s:%s'" % (req.get_full_url(), req.get_data()))
117 res = urllib2.urlopen(req)
118 content = res.read()
119 res.close()
120 return (res, content)
121 except:
122 print_exception(traceback.format_exc())
123 return (None, None)
124
125def ofdb_url_get(url, data = None, as_post = False):
126 (rc, content) = _myth_url_get(url, data, as_post)
127
128 m = re.search(r'<\s*meta[^>]*charset\s*=\s*([^" ]+)', content, re.I)
129 if m:
130 charset = m.group(1)
131 debug_out("Page charset reported as %s" % (charset))
132 # The page lies about encoding (often using two character
133 # encodings on the same page).
134 content = _xmlprep(unicode(content, charset, 'replace')).encode("utf8")
135 else:
136 # hope it is ascii
137 content = _xmlprep(unicode(content, errors='replace')).encode("utf8")
138
139 response_out(content)
140 return (rc, content)
141
142def search_title(title):
143 def clean_title(t):
144 t = urllib.unquote(t)
145 (t, ext) = os.path.splitext(t)
146 m = re.match("(.*)(?:[(|\[]|, The$)",t, re.I)
147 ret = t
148 if m:
149 ret = m.group(1)
150 return ret.strip().encode("utf8")
151
152 try:
153 data = {
154 "page" : "suchergebnis",
155 "Kat" : "DTitel",
156 "SText" : clean_title(title)
157 }
158
159 debug_out("Starting search for title '%s'" % (title,))
160
161 (rc, content) = ofdb_url_get(urlparse.urljoin(URL_BASE, "view.php"),
162 data, True)
163
164 reader = HtmlLib.Reader()
165 doc = reader.fromString(content, charset='utf8')
166
167 nodes = xpath.Evaluate("//A[starts-with(@href, 'film/')]",
168 doc.documentElement)
169
170 title_matches = []
171 uid_match = re.compile('/(\d+),.*', re.I)
172 for title in nodes:
173 rm = uid_match.search(title.getAttributeNS(EMPTY_NAMESPACE, 'href'))
174 if rm:
175 title_matches.append((rm.group(1), title.firstChild.nodeValue))
176
177 for id, title in title_matches:
178 print("%s:%s" % (id, title.strip()))
179 except:
180 print_exception(traceback.format_exc())
181
182def get_ofdb_doc(uid, context):
183 """Returns the OFDb film page as an XML document."""
184 debug_out("Starting search for %s '%s'" % (context, uid))
185
186 (rc, content) = ofdb_url_get(urlparse.urljoin(URL_BASE,
187 "film/%s," % (uid.encode("utf8"),)))
188
189 reader = HtmlLib.Reader()
190 return reader.fromString(content, charset='utf8')
191
192class NoIMDBURL(Exception):
193 pass
194
195def search_data(uid, rating_country):
196 def possible_error(path):
197 comment_out("Warning: expected to find content at '%s', site format " \
198 "may have changed, look for a new version of this script." %
199 (path,))
200
201 def single_value(doc, path):
202 nodes = xpath.Evaluate(path, doc)
203 if len(nodes):
204 return nodes[0].firstChild.nodeValue.strip()
205 possible_error(path)
206 return ""
207
208 def attr_value(doc, path, attrname):
209 nodes = xpath.Evaluate(path, doc)
210 if len(nodes):
211 return nodes[0].getAttributeNS(EMPTY_NAMESPACE, attrname).strip()
212 possible_error(path)
213 return ""
214
215 def multi_value(doc, path):
216 ret = []
217 nodes = xpath.Evaluate(path, doc)
218 if len(nodes):
219 for i in nodes:
220 ret.append(i.firstChild.nodeValue.strip())
221 return ret
222 possible_error(path)
223 return ""
224
225 def direct_value(doc, path):
226 nodes = xpath.Evaluate(path, doc)
227 if len(nodes):
228 return nodes[0].nodeValue.strip()
229 possible_error(path)
230 return ""
231
232 def all_text_children(doc, path):
233 nodes = xpath.Evaluate(path, doc)
234 if len(nodes):
235 ret = []
236 for n in nodes:
237 for c in n.childNodes:
238 if c.nodeType == c.TEXT_NODE:
239 ret.append(c.nodeValue.strip())
240 return " ".join(ret)
241 possible_error(path)
242 return ""
243
244 try:
245 doc = get_ofdb_doc(uid, "data")
246
247#TODO: Add details from tmdb
248 data = {'title' : '',
249 'countries' : '',
250 'year' : '',
251 'directors' : '',
252 'cast' : '',
253 'genre' : '',
254 'user_rating' : '',
255# 'movie_rating' : '',
256 'plot' : '',
257# 'release_date' : '',
258# 'runtime' : '',
259# 'writers' : '',
260 }
261
262 data['title'] = single_value(doc.documentElement,
263 "//TD[@width='99%']/H2/FONT[@size='3']/B")
264 data['countries'] = ",".join(multi_value(doc.documentElement,
265 "//A[starts-with(@href, 'view.php?page=blaettern&Kat=Land&')]"))
266 data['year'] = single_value(doc.documentElement,
267 "//A[starts-with(@href, 'view.php?page=blaettern&Kat=Jahr&')]")
268 data['directors'] = ",".join(multi_value(doc.documentElement,
269 "//TD[@width='99%']/TABLE/TR[4]/TD[3]//A[starts-with(@href, " \
270 "'view.php?page=liste')]"))
271 data['cast'] = ",".join(multi_value(doc.documentElement,
272 "//TD[@width='99%']/TABLE/TR[5]/TD[3]//A[starts-with(@href, " \
273 "'view.php?page=liste')]"))
274 data['genre'] = ",".join(multi_value(doc.documentElement,
275 "//A[starts-with(@href, 'view.php?page=genre&Genre=')]"))
276 data['user_rating'] = attr_value(doc.documentElement,
277 "//IMG[@src='images/design3/notenspalte.png']", "alt")
278
279 tmp_sid = attr_value(doc.documentElement,
280 "//A[starts-with(@href, 'plot/')]", "href")
281
282 sid_match = re.search("/(\d+,\d+,.*)", tmp_sid, re.I)
283 sid = None
284 if sid_match:
285 sid = sid_match.group(1)
286
287 debug_out("Looking for plot...")
288 (rc, content) = ofdb_url_get(urlparse.urljoin(URL_BASE,
289 "plot/%s" % sid.encode("utf8")))
290
291 reader = HtmlLib.Reader()
292 doc = reader.fromString(content, charset='utf8')
293
294 data['plot'] = unicode(all_text_children(doc.documentElement,
295 "//FONT[@class='Blocksatz']"))
296
297#ReleaseDate:%(release_date)s
298#MovieRating:%(movie_rating)s
299#Runtime:%(runtime)s
300#Writers:%(writers)s
301 print("""\
302Title:%(title)s
303Year:%(year)s
304Director:%(directors)s
305Plot:%(plot)s
306UserRating:%(user_rating)s
307Cast:%(cast)s
308Genres:%(genre)s
309Countries:%(countries)s
310""" % data)
311
312 except:
313 print_exception(traceback.format_exc())
314
315def search_poster(uid):
316 try:
317 debug_out("Looking for posters...")
318 poster_urls = []
319 ofdoc = get_ofdb_doc(uid, "poster")
320
321 nodes = xpath.Evaluate("//IMG[starts-with(@src, 'http://img.ofdb.de/film/')]",
322 ofdoc.documentElement)
323 for node in nodes:
324 poster_urls.append(node.getAttributeNS(EMPTY_NAMESPACE, 'src'))
325
326 for p in poster_urls:
327 print(p)
328 except:
329 print_exception(traceback.format_exc())
330
331def main():
332 parser = OptionParser(usage="""\
333Usage: %prog [-M TITLE | -D UID [-R COUNTRY[,COUNTRY]] | -P UID]
334""", version="%%prog %s" % (ofdb_version))
335 parser.add_option("-M", "--title", type="string", dest="title_search",
336 metavar="TITLE", help="Search for TITLE")
337 parser.add_option("-D", "--data", type="string", dest="data_search",
338 metavar="UID", help="Search for video data for UID")
339 parser.add_option("-R", "--rating-country", type="string",
340 dest="ratings_from", metavar="COUNTRY",
341 help="When retrieving data, use ratings from COUNTRY")
342 parser.add_option("-P", "--poster", type="string", dest="poster_search",
343 metavar="UID", help="Search for images associated with UID")
344 parser.add_option("-d", "--debug", action="store_true", dest="verbose",
345 default=False, help="Display debug information")
346 parser.add_option("-r", "--dump-response", action="store_true",
347 dest="dump_response", default=False,
348 help="Output the raw response")
349
350 (options, args) = parser.parse_args()
351
352 global VERBOSE, DUMP_RESPONSE
353 VERBOSE = options.verbose
354 DUMP_RESPONSE = options.dump_response
355
356 if options.title_search:
357 search_title(unicode(options.title_search, "utf8"))
358 elif options.data_search:
359 rf = options.ratings_from
360 if rf:
361 rf = unicode(rf, "utf8")
362 search_data(unicode(options.data_search, "utf8"), rf)
363 elif options.poster_search:
364 search_poster(unicode(options.poster_search, "utf8"))
365 else:
366 parser.print_usage()
367 sys.exit(1)
368
369if __name__ == '__main__':
370 try:
371 codecinfo = codecs.lookup('utf8')
372
373 u2utf8 = codecinfo.streamwriter(sys.stdout)
374 sys.stdout = u2utf8
375
376 main()
377 except SystemExit:
378 pass
379 except:
380 print_exception(traceback.format_exc())
381
382# vim: ts=4 sw=4:
383