1 # -*- coding: UTF-8 -*-
2 from Plugins.Plugin import PluginDescriptor
3 from twisted.web.client import downloadPage
4 from enigma import ePicLoad, eServiceReference
5 from Screens.Screen import Screen
6 from Screens.EpgSelection import EPGSelection
7 from Screens.ChannelSelection import SimpleChannelSelection
8 from Components.ActionMap import ActionMap
9 from Components.Pixmap import Pixmap
10 from Components.Label import Label
11 from Components.ScrollLabel import ScrollLabel
12 from Components.Button import Button
13 from Components.AVSwitch import AVSwitch
14 from Components.MenuList import MenuList
15 from Components.Language import language
16 from Components.ProgressBar import ProgressBar
17 from Tools.Directories import resolveFilename, SCOPE_PLUGINS, SCOPE_SKIN_IMAGE
18 from os import environ as os_environ
25 lang = language.getLanguage()[:2] # getLanguage returns e.g. "fi_FI" for "language_country"
26 os_environ["LANGUAGE"] = lang # Enigma doesn't set this (or LC_ALL, LC_MESSAGES, LANG). gettext needs it!
27 gettext.bindtextdomain("IMDb", resolveFilename(SCOPE_PLUGINS, "Extensions/IMDb/locale"))
30 t = gettext.dgettext("IMDb", txt)
32 print "[IMDb] fallback to default translation for", txt
33 t = gettext.gettext(txt)
37 language.addCallback(localeInit)
39 class IMDBChannelSelection(SimpleChannelSelection):
40 def __init__(self, session):
41 SimpleChannelSelection.__init__(self, session, _("Channel Selection"))
42 self.skinName = "SimpleChannelSelection"
44 self["ChannelSelectEPGActions"] = ActionMap(["ChannelSelectEPGActions"],
46 "showEPGList": self.channelSelected
50 def channelSelected(self):
51 ref = self.getCurrentSelection()
52 if (ref.flags & 7) == 7:
54 elif not (ref.flags & eServiceReference.isMarker):
55 self.session.openWithCallback(
62 def epgClosed(self, ret = None):
66 class IMDBEPGSelection(EPGSelection):
67 def __init__(self, session, ref, openPlugin = True):
68 EPGSelection.__init__(self, session, ref)
69 self.skinName = "EPGSelection"
70 self["key_green"].setText(_("Lookup"))
71 self.openPlugin = openPlugin
73 def infoKeyPressed(self):
77 cur = self["list"].getCurrent()
89 self.close(evt.getEventName())
91 def onSelectionChanged(self):
96 <screen name="IMDB" position="90,95" size="560,420" title="Internet Movie Database Details Plugin" >
97 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" zPosition="0" size="140,40" transparent="1" alphatest="on" />
98 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" zPosition="0" size="140,40" transparent="1" alphatest="on" />
99 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" zPosition="0" size="140,40" transparent="1" alphatest="on" />
100 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" zPosition="0" size="140,40" transparent="1" alphatest="on" />
101 <widget name="key_red" position="0,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#9f1313" transparent="1" />
102 <widget name="key_green" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
103 <widget name="key_yellow" position="280,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#a08500" transparent="1" />
104 <widget name="key_blue" position="420,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#18188b" transparent="1" />
105 <widget name="titellabel" position="10,40" size="330,45" valign="center" font="Regular;22"/>
106 <widget name="detailslabel" position="105,90" size="445,140" font="Regular;18" />
107 <widget name="castlabel" position="10,235" size="540,155" font="Regular;18" />
108 <widget name="extralabel" position="10,40" size="540,350" font="Regular;18" />
109 <widget name="ratinglabel" position="340,62" size="210,20" halign="center" font="Regular;18" foregroundColor="#f0b400"/>
110 <widget name="statusbar" position="10,404" size="540,16" font="Regular;16" foregroundColor="#cccccc" />
111 <widget name="poster" position="4,90" size="96,140" alphatest="on" />
112 <widget name="menu" position="10,115" size="540,275" zPosition="3" scrollbarMode="showOnDemand" />
113 <widget name="starsbg" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/IMDb/starsbar_empty.png" position="340,40" zPosition="0" size="210,21" transparent="1" alphatest="on" />
114 <widget name="stars" position="340,40" size="210,21" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/IMDb/starsbar_filled.png" transparent="1" />
117 def __init__(self, session, eventName, callbackNeeded=False):
118 Screen.__init__(self, session)
120 self.eventName = eventName
122 self.callbackNeeded = callbackNeeded
123 self.callbackData = ""
124 self.callbackGenre = ""
126 self.dictionary_init()
128 self["poster"] = Pixmap()
129 self.picload = ePicLoad()
130 self.picload.PictureData.get().append(self.paintPosterPixmapCB)
132 self["stars"] = ProgressBar()
133 self["starsbg"] = Pixmap()
135 self["starsbg"].hide()
136 self.ratingstars = -1
138 self["titellabel"] = Label(_("The Internet Movie Database"))
139 self["detailslabel"] = ScrollLabel("")
140 self["castlabel"] = ScrollLabel("")
141 self["extralabel"] = ScrollLabel("")
142 self["statusbar"] = Label("")
143 self["ratinglabel"] = Label("")
145 self["menu"] = MenuList(self.resultlist)
148 self["key_red"] = Button(_("Exit"))
149 self["key_green"] = Button("")
150 self["key_yellow"] = Button("")
151 self["key_blue"] = Button("")
153 # 0 = multiple query selection menu page
154 # 1 = movie info page
155 # 2 = extra infos page
158 self["actions"] = ActionMap(["OkCancelActions", "ColorActions", "MovieSelectionActions", "DirectionActions"],
160 "ok": self.showDetails,
162 "down": self.pageDown,
165 "green": self.showMenu,
166 "yellow": self.showDetails,
167 "blue": self.showExtras,
168 "contextMenu": self.openChannelSelection,
169 "showEventInfo": self.showDetails
175 if self.callbackNeeded:
176 self.close([self.callbackData, self.callbackGenre])
180 def dictionary_init(self):
181 syslang = language.getLanguage()
182 if "de" not in syslang:
183 self.IMDBlanguage = "" # set to empty ("") for english version
185 self.IMDBlanguage = "german." # it's a subdomain, so add a '.' at the end
187 self.htmltags = re.compile('<.*?>')
189 self.generalinfomask = re.compile(
190 '<h1>(?P<title>.*?) <.*?</h1>.*?'
191 '(?:.*?<h5>(?P<g_director>Regisseur|Directors?):</h5>.*?<a href=\".*?\">(?P<director>.*?)</a>)*'
192 '(?:.*?<h5>(?P<g_creator>Sch\S*?pfer|Creators?):</h5>.*?<a href=\".*?\">(?P<creator>.*?)</a>)*'
193 '(?:.*?<h5>(?P<g_seasons>Seasons):</h5>(?:.*?)<a href=\".*?\">(?P<seasons>\d+?)</a>\s+?(?:<a class|\|\s+?<a href="episodes#season-unknown))*'
194 '(?:.*?<h5>(?P<g_writer>Drehbuch|Writer).*?</h5>.*?<a href=\".*?\">(?P<writer>.*?)</a>)*'
195 '(?:.*?<h5>(?P<g_premiere>Premiere|Release Date).*?</h5>\s+<div.*?>\s?(?P<premiere>.*?)\n\s.*?<)*'
196 '(?:.*?<h5>(?P<g_alternativ>Auch bekannt als|Also Known As):</h5><div.*?>\s*(?P<alternativ>.*?)<br>)*'#\s{0,8}<a.*?>(?:Mehr|See more))*'
197 '(?:.*?<h5>(?P<g_country>Land|Country):</h5>\s+<div.*?>(?P<country>.*?)</div>(?:.*?Mehr|\s+?</div>))*'
200 self.extrainfomask = re.compile(
201 '(?:.*?<h5>(?P<g_tagline>Werbezeile|Tagline?):</h5>\n(?P<tagline>.+?)<)*'
202 '(?:.*?<h5>(?P<g_outline>Kurzbeschreibung|Plot Outline):</h5>(?P<outline>.+?)<)*'
203 '(?:.*?<h5>(?P<g_synopsis>Plot Synopsis):</h5>(?:.*?)(?:<a href=\".*?\">)*?(?P<synopsis>.+?)(?:</a>|</div>))*'
204 '(?:.*?<h5>(?P<g_keywords>Plot Keywords):</h5>(?P<keywords>.+?)(?:Mehr|See more</a>|</div>))*'
205 '(?:.*?<h5>(?P<g_awards>Filmpreise|Awards):</h5>(?P<awards>.+?)(?:Mehr|See more</a>|</div>))*'
206 '(?:.*?<h5>(?P<g_runtime>L\S*?nge|Runtime):</h5>(?P<runtime>.+?)</div>)*'
207 '(?:.*?<h5>(?P<g_language>Sprache|Language):</h5>(?P<language>.+?)</div>)*'
208 '(?:.*?<h5>(?P<g_color>Farbe|Color):</h5>(?P<color>.+?)</div>)*'
209 '(?:.*?<h5>(?P<g_aspect>Seitenverh\S*?ltnis|Aspect Ratio):</h5>(?P<aspect>.+?)(?:Mehr|See more</a>|</div>))*'
210 '(?:.*?<h5>(?P<g_sound>Tonverfahren|Sound Mix):</h5>(?P<sound>.+?)</div>)*'
211 '(?:.*?<h5>(?P<g_cert>Altersfreigabe|Certification):</h5>(?P<cert>.+?)</div>)*'
212 '(?:.*?<h5>(?P<g_locations>Drehorte|Filming Locations):</h5>(?P<locations>.+?)(?:Mehr|See more</a>|</div>))*'
213 '(?:.*?<h5>(?P<g_company>Firma|Company):</h5>(?P<company>.+?)(?:Mehr|See more</a>|</div>))*'
214 '(?:.*?<h5>(?P<g_trivia>Dies und das|Trivia):</h5>(?P<trivia>.+?)(?:Mehr|See more</a>|</div>))*'
215 '(?:.*?<h5>(?P<g_goofs>Pannen|Goofs):</h5>(?P<goofs>.+?)(?:Mehr|See more</a>|</div>))*'
216 '(?:.*?<h5>(?P<g_quotes>Dialogzitate|Quotes):</h5>(?P<quotes>.+?)(?:Mehr|See more</a>|</div>))*'
217 '(?:.*?<h5>(?P<g_connections>Bez\S*?ge zu anderen Titeln|Movie Connections):</h5>(?P<connections>.+?)(?:Mehr|See more</a>|</div>))*'
218 '(?:.*?<h3>(?P<g_comments>Nutzerkommentare|User Comments)</h3>.*?<a href="/user/ur\d{7,7}/comments">(?P<commenter>.+?)\n</div>.*?<p>(?P<comment>.+?)</p>)*'
221 def resetLabels(self):
222 self["detailslabel"].setText("")
223 self["ratinglabel"].setText("")
224 self["titellabel"].setText("")
225 self["castlabel"].setText("")
226 self["titellabel"].setText("")
227 self["extralabel"].setText("")
228 self.ratingstars = -1
232 self["menu"].instance.moveSelection(self["menu"].instance.moveUp)
234 self["castlabel"].pageUp()
235 self["detailslabel"].pageUp()
237 self["extralabel"].pageUp()
241 self["menu"].instance.moveSelection(self["menu"].instance.moveDown)
243 self["castlabel"].pageDown()
244 self["detailslabel"].pageDown()
246 self["extralabel"].pageDown()
249 if ( self.Page is 1 or self.Page is 2 ) and self.resultlist:
252 self["starsbg"].hide()
253 self["ratinglabel"].hide()
254 self["castlabel"].hide()
255 self["poster"].hide()
256 self["extralabel"].hide()
257 self["titellabel"].setText(_("Ambiguous results"))
258 self["detailslabel"].setText(_("Please select the matching entry"))
259 self["detailslabel"].show()
260 self["key_blue"].setText("")
261 self["key_green"].setText(_("Title Menu"))
262 self["key_yellow"].setText(_("Details"))
265 def showDetails(self):
266 self["ratinglabel"].show()
267 self["castlabel"].show()
268 self["detailslabel"].show()
270 if self.resultlist and self.Page == 0:
271 link = self["menu"].getCurrent()[1]
272 title = self["menu"].getCurrent()[0]
273 self["statusbar"].setText(_("Re-Query IMDb: %s...") % (title))
274 localfile = "/tmp/imdbquery2.html"
275 fetchurl = "http://" + self.IMDBlanguage + "imdb.com/title/" + link
276 print "[IMDB] downloading query " + fetchurl + " to " + localfile
277 downloadPage(fetchurl,localfile).addCallback(self.IMDBquery2).addErrback(self.fetchFailed)
283 self["extralabel"].hide()
284 self["poster"].show()
285 if self.ratingstars > 0:
286 self["starsbg"].show()
288 self["stars"].setValue(self.ratingstars)
292 def showExtras(self):
294 self["extralabel"].show()
295 self["detailslabel"].hide()
296 self["castlabel"].hide()
297 self["poster"].hide()
299 self["starsbg"].hide()
300 self["ratinglabel"].hide()
303 def openChannelSelection(self):
304 self.session.openWithCallback(
305 self.channelSelectionClosed,
309 def channelSelectionClosed(self, ret = None):
315 self["ratinglabel"].show()
316 self["castlabel"].show()
317 self["detailslabel"].show()
318 self["poster"].hide()
320 self["starsbg"].hide()
325 if self.eventName is "":
326 s = self.session.nav.getCurrentService()
328 event = info.getEvent(0) # 0 = now, 1 = next
330 self.eventName = event.getEventName()
331 if self.eventName is not "":
332 self["statusbar"].setText(_("Query IMDb: %s...") % (self.eventName))
333 event_quoted = urllib.quote(self.eventName.decode('utf8').encode('latin-1','ignore'))
334 localfile = "/tmp/imdbquery.html"
335 fetchurl = "http://" + self.IMDBlanguage + "imdb.com/find?q=" + event_quoted + "&s=tt&site=aka"
336 print "[IMDB] Downloading Query " + fetchurl + " to " + localfile
337 downloadPage(fetchurl,localfile).addCallback(self.IMDBquery).addErrback(self.fetchFailed)
339 self["statusbar"].setText(_("Could't get Eventname"))
341 def fetchFailed(self,string):
342 print "[IMDB] fetch failed", string
343 self["statusbar"].setText(_("IMDb Download failed"))
345 def html2utf8(self,in_html):
346 htmlentitynumbermask = re.compile('(&#(\d{1,5}?);)')
347 htmlentityhexmask = re.compile('(&#x([0-9A-Fa-f]{2,2}?);)')
348 htmlentitynamemask = re.compile('(&([^#]\D{1,5}?);)')
351 entities = htmlentitynamemask.finditer(in_html)
354 entitydict[x.group(1)] = x.group(2)
355 for key, name in entitydict.items():
356 entitydict[key] = htmlentitydefs.name2codepoint[name]
357 entities = htmlentityhexmask.finditer(in_html)
360 entityhexdict[x.group(1)] = x.group(2)
362 for key, name in entityhexdict.items():
363 entitydict[key] = "%d" % int(key[3:5], 16)
364 print "key:", key, "before:", name, "after:", entitydict[key]
366 entities = htmlentitynumbermask.finditer(in_html)
368 entitydict[x.group(1)] = x.group(2)
369 for key, codepoint in entitydict.items():
370 in_html = in_html.replace(key, (unichr(int(codepoint)).encode('latin-1')))
371 self.inhtml = in_html.decode('latin-1').encode('utf8')
373 def IMDBquery(self,string):
375 self["statusbar"].setText(_("IMDb Download completed"))
377 self.html2utf8(open("/tmp/imdbquery.html", "r").read())
379 self.generalinfos = self.generalinfomask.search(self.inhtml)
381 if self.generalinfos:
384 if re.search("<title>(?:IMDb.{0,9}Search|IMDb Titelsuche)</title>", self.inhtml):
385 searchresultmask = re.compile("<tr> <td.*?img src.*?>.*?<a href=\".*?/title/(tt\d{7,7})/\".*?>(.*?)</td>", re.DOTALL)
386 searchresults = searchresultmask.finditer(self.inhtml)
387 self.resultlist = [(self.htmltags.sub('',x.group(2)), x.group(1)) for x in searchresults]
388 self["menu"].l.setList(self.resultlist)
389 if len(self.resultlist) > 1:
393 self["detailslabel"].setText(_("No IMDb match."))
394 self["statusbar"].setText(_("No IMDb match."))
396 splitpos = self.eventName.find('(')
397 if splitpos > 0 and self.eventName.endswith(')'):
398 self.eventName = self.eventName[splitpos+1:-1]
399 self["statusbar"].setText(_("Re-Query IMDb: %s...") % (self.eventName))
400 event_quoted = urllib.quote(self.eventName.decode('utf8').encode('latin-1','ignore'))
401 localfile = "/tmp/imdbquery.html"
402 fetchurl = "http://" + self.IMDBlanguage + "imdb.com/find?q=" + event_quoted + "&s=tt&site=aka"
403 print "[IMDB] Downloading Query " + fetchurl + " to " + localfile
404 downloadPage(fetchurl,localfile).addCallback(self.IMDBquery).addErrback(self.fetchFailed)
406 self["detailslabel"].setText(_("IMDb query failed!"))
408 def IMDBquery2(self,string):
409 self["statusbar"].setText(_("IMDb Re-Download completed"))
410 self.html2utf8(open("/tmp/imdbquery2.html", "r").read())
411 self.generalinfos = self.generalinfomask.search(self.inhtml)
417 Detailstext = _("No details found.")
418 if self.generalinfos:
419 self["key_yellow"].setText(_("Details"))
420 self["statusbar"].setText(_("IMDb Details parsed"))
421 Titeltext = self.generalinfos.group("title")
422 if len(Titeltext) > 57:
423 Titeltext = Titeltext[0:54] + "..."
424 self["titellabel"].setText(Titeltext)
428 genreblockmask = re.compile('<h5>Genre:</h5>\s<div class="info-content">\s+?(.*?)\s+?(?:Mehr|See more|</p|<a class|</div>)', re.DOTALL)
429 genreblock = genreblockmask.findall(self.inhtml)
431 genres = self.htmltags.sub('', genreblock[0])
433 Detailstext += "Genre: "
434 Detailstext += genres
435 self.callbackGenre = genres
437 for category in ("director", "creator", "writer", "premiere", "seasons"):
438 if self.generalinfos.group('g_'+category):
439 Detailstext += "\n" + self.generalinfos.group('g_'+category) + ": " + self.generalinfos.group(category)
441 if self.generalinfos.group("country"):
442 Detailstext += "\n" + self.generalinfos.group("g_country") + ": " + self.htmltags.sub('', self.generalinfos.group("country").replace('\n','').replace("<br>",'\n').replace(" ",' '))
444 if self.generalinfos.group("alternativ"):
445 Detailstext += "\n" + self.generalinfos.group("g_alternativ") + ": " + self.htmltags.sub('', self.generalinfos.group("alternativ").replace('\n','').replace("<br>",'\n').replace(" ",' '))
447 ratingmask = re.compile('<h5>(?P<g_rating>Nutzer-Bewertung|User Rating):</h5>.*?<b>(?P<rating>.*?)/10</b>', re.DOTALL)
448 rating = ratingmask.search(self.inhtml)
449 Ratingtext = _("no user rating yet")
451 Ratingtext = rating.group("g_rating") + ": " + rating.group("rating") + " / 10"
452 self.ratingstars = int(10*round(float(rating.group("rating").replace(',','.')),1))
454 self["stars"].setValue(self.ratingstars)
455 self["starsbg"].show()
456 self["ratinglabel"].setText(Ratingtext)
457 castmask = re.compile('<td class="nm">.*?>(.*?)</a>.*?<td class="char">(?:<a.*?>)?(.*?)(?:</a>)?</td>', re.DOTALL)
458 castresult = castmask.finditer(self.inhtml)
462 Casttext += "\n" + self.htmltags.sub('', x.group(1))
464 Casttext += _(" as ") + self.htmltags.sub('', x.group(2).replace('/ ...',''))
465 if Casttext is not "":
466 Casttext = _("Cast: ") + Casttext
468 Casttext = _("No cast list found in the database.")
469 self["castlabel"].setText(Casttext)
470 postermask = re.compile('<div class="photo">.*?<img .*? src=\"(http.*?)\" .*?>', re.DOTALL)
471 posterurl = postermask.search(self.inhtml)
472 if posterurl and posterurl.group(1).find("jpg") > 0:
473 posterurl = posterurl.group(1)
474 self["statusbar"].setText(_("Downloading Movie Poster: %s...") % (posterurl))
475 localfile = "/tmp/poster.jpg"
476 print "[IMDB] downloading poster " + posterurl + " to " + localfile
477 downloadPage(posterurl,localfile).addCallback(self.IMDBPoster).addErrback(self.fetchFailed)
479 self.IMDBPoster("kein Poster")
480 extrainfos = self.extrainfomask.search(self.inhtml)
483 Extratext = "Extra Info\n"
485 for category in ("tagline","outline","synopsis","keywords","awards","runtime","language","color","aspect","sound","cert","locations","company","trivia","goofs","quotes","connections"):
486 if extrainfos.group('g_'+category):
487 Extratext += extrainfos.group('g_'+category) + ": " + self.htmltags.sub('',extrainfos.group(category).replace("\n",'').replace("<br>",'\n')) + "\n"
488 if extrainfos.group("g_comments"):
489 stripmask = re.compile('\s{2,}', re.DOTALL)
490 Extratext += extrainfos.group("g_comments") + " [" + stripmask.sub(' ', self.htmltags.sub('',extrainfos.group("commenter"))) + "]: " + self.htmltags.sub('',extrainfos.group("comment").replace("\n",' ')) + "\n"
492 self["extralabel"].setText(Extratext)
493 self["extralabel"].hide()
494 self["key_blue"].setText(_("Extra Info"))
496 self["detailslabel"].setText(Detailstext)
497 self.callbackData = Detailstext
499 def IMDBPoster(self,string):
500 self["statusbar"].setText(_("IMDb Details parsed"))
502 filename = "/tmp/poster.jpg"
504 filename = resolveFilename(SCOPE_PLUGINS, "Extensions/IMDb/no_poster.png")
505 sc = AVSwitch().getFramebufferScale()
506 self.picload.setPara((self["poster"].instance.size().width(), self["poster"].instance.size().height(), sc[0], sc[1], False, 1, "#00000000"))
507 self.picload.startDecode(filename)
509 def paintPosterPixmapCB(self, picInfo=None):
510 ptr = self.picload.getData()
512 self["poster"].instance.setPixmap(ptr.__deref__())
513 self["poster"].show()
515 def createSummary(self):
518 class IMDbLCDScreen(Screen):
520 <screen position="0,0" size="132,64" title="IMDB Plugin">
521 <widget name="headline" position="4,0" size="128,22" font="Regular;20"/>
522 <widget source="session.Event_Now" render="Label" position="6,26" size="120,34" font="Regular;14" >
523 <convert type="EventName">Name</convert>
527 def __init__(self, session, parent):
528 Screen.__init__(self, session)
529 self["headline"] = Label(_("IMDb Plugin"))
531 def eventinfo(session, servicelist, **kwargs):
532 ref = session.nav.getCurrentlyPlayingServiceReference()
533 session.open(IMDBEPGSelection, ref)
535 def main(session, eventName="", **kwargs):
536 session.open(IMDB, eventName)
538 def Plugins(**kwargs):
540 return [PluginDescriptor(name="IMDb Details",
541 description=_("Query details from the Internet Movie Database"),
543 where = PluginDescriptor.WHERE_PLUGINMENU,
545 PluginDescriptor(name="IMDb Details",
546 description=_("Query details from the Internet Movie Database"),
547 where = PluginDescriptor.WHERE_EVENTINFO,
550 except AttributeError:
551 wherelist = [PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_PLUGINMENU]
552 return PluginDescriptor(name="IMDb Details",
553 description=_("Query details from the Internet Movie Database"),