]>
Commit | Line | Data |
---|---|---|
489423d6 ER |
1 | # -*- coding: utf-8 -*- |
2 | ||
3 | __revision__ = '$Id: $' | |
4 | ||
5 | # Copyright (c) 2015 | |
6 | ||
7 | # This program is free software; you can redistribute it and/or modify | |
8 | # it under the terms of the GNU General Public License as published by | |
9 | # the Free Software Foundation; either version 2 of the License, or | |
10 | # (at your option) any later version. | |
11 | # | |
12 | # This program is distributed in the hope that it will be useful, | |
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | # GNU Library General Public License for more details. | |
16 | # | |
17 | # You should have received a copy of the GNU General Public License | |
18 | # along with this program; if not, write to the Free Software | |
19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA | |
20 | ||
21 | # You may use and distribute this software under the terms of the | |
22 | # GNU General Public License, version 2 or later | |
23 | ||
24 | from plugins.imp import ImportPlugin as IP | |
25 | import os | |
26 | import gutils | |
489423d6 | 27 | from lxml import etree |
0a532283 ER |
28 | import tempfile |
29 | from urllib2 import urlopen, URLError, HTTPError | |
489423d6 ER |
30 | |
31 | import logging | |
32 | log = logging.getLogger("Griffith") | |
33 | ||
34 | """ | |
880cc34c ER |
35 | Supports importing videodb.xml exported as single file from Kodi/XBMC. |
36 | ||
37 | http://kodi.wiki/view/Import-export_library | |
38 | http://kodi.wiki/view/HOW-TO:Backup_the_video_library | |
39 | ||
489423d6 ER |
40 | See lib/plugins/imp/__init__.py for workflow how importer invokes methods from importer plugins |
41 | """ | |
42 | class ImportPlugin(IP): | |
43 | description = 'Kodi/XBMC MovieDB importer' | |
44 | author = 'Elan Ruusamäe' | |
45 | email = 'glen@pld-linux.org' | |
46 | version = '1.0' | |
47 | file_filters = 'videodb.xml' | |
48 | mime_types = None | |
49 | ||
50 | fileversion = None | |
51 | xml = None | |
52 | items = None | |
53 | itemindex = 0 | |
0a532283 | 54 | poster_file = None |
489423d6 ER |
55 | |
56 | # used by get_movie_details method | |
57 | # griffith field => kodi field | |
58 | # griffith field is actual SQL column in 'movies' table | |
59 | field_map = { | |
60 | 'title': 'title', | |
61 | 'o_title': 'originaltitle', | |
62 | 'year': 'year', | |
5411d3e0 | 63 | 'runtime': 'runtime', |
489423d6 ER |
64 | 'rating': 'rating', |
65 | 'plot': 'plot', | |
66 | 'director': 'director', | |
67 | 'studio': 'studio', | |
68 | 'country': 'country', | |
91cfb43d | 69 | 'classification': 'mpaa', |
489423d6 ER |
70 | # while the trailer field exists, it is not useful for griffith |
71 | # as it's something like: "plugin://plugin.video.youtube/?action=play_video&videoid=..." | |
72 | # however youtube urls can be probably fixed. | |
73 | 'trailer': 'trailer', | |
74 | } | |
75 | ||
91cfb43d ER |
76 | # rest of the stuff to insert into notes field |
77 | notes_map = { | |
78 | _('Id') : 'id', | |
79 | _('Play count'): 'playcount', | |
80 | _('Date added'): 'dateadded', | |
81 | _('Last played'): 'lastplayed', | |
82 | _('Collection'): 'set', | |
83 | _('Premiered'): 'premiered', | |
84 | _('Aired'): 'aired', | |
85 | } | |
86 | ||
489423d6 ER |
87 | def initialize(self): |
88 | if not IP.initialize(self): | |
89 | return False | |
90 | self.edit = False | |
91 | return True | |
92 | ||
93 | def set_source(self, name): | |
94 | IP.set_source(self, name) | |
95 | self.filename = name | |
96 | self.fileversion = self.read_fileversion() | |
97 | if self.fileversion == None: | |
98 | gutils.error(_('The format of the file is not supported.')) | |
99 | return False | |
100 | return True | |
101 | ||
102 | def clear(self): | |
103 | """clear plugin state before next source file""" | |
104 | IP.clear(self) | |
105 | if self.xml: | |
106 | self.xml = None | |
107 | self.fileversion = None | |
108 | self.items = None | |
109 | self.itemindex = 0 | |
0a532283 ER |
110 | if self.poster_file: |
111 | os.unlink(self.poster_file) | |
112 | self.poster_file = None | |
489423d6 ER |
113 | |
114 | def destroy(self): | |
115 | """close all resources""" | |
116 | IP.destroy(self) | |
117 | ||
118 | def read_fileversion(self): | |
119 | version = None | |
120 | try: | |
121 | self.xml = etree.parse(self.filename) | |
122 | version = self.xml.xpath('/videodb/version')[0].text | |
123 | except Exception, e: | |
124 | log.error(str(e)) | |
125 | log.info('Found file version %s' % version) | |
126 | return version | |
127 | ||
128 | def count_movies(self): | |
129 | """Returns number of movies in file which is about to be imported""" | |
130 | if not self.xml: | |
131 | log.error('No XML object') | |
132 | return 0 | |
133 | ||
134 | count = 0 | |
135 | ||
136 | try: | |
137 | count = int(self.xml.xpath('count(/videodb/movie)')) | |
138 | except Exception, e: | |
139 | log.exception(e) | |
140 | ||
141 | log.info('%s movies for import' % count) | |
142 | return count | |
143 | ||
144 | def get_movie_details(self): | |
145 | """Returns dictionary with movie details""" | |
146 | if not self.xml: | |
147 | log.error('XML not opened') | |
148 | return None | |
149 | ||
150 | if not self.items: | |
151 | self.items = self.xml.xpath('/videodb/movie') | |
152 | self.itemindex = 0 | |
153 | ||
154 | # don't bother for empty db (shouldn't happen really) | |
155 | if not self.items or len(self.items) < 1: | |
156 | return None | |
157 | ||
158 | # no more items | |
159 | if self.itemindex >= len(self.items): | |
160 | return None | |
161 | ||
162 | item = self.items[self.itemindex] | |
163 | ||
164 | # fill details | |
165 | details = {} | |
166 | for k,v in self.field_map.items(): | |
167 | details[k] = item.findtext(v) | |
168 | ||
169 | # if playcount set, means it's seen | |
170 | details['seen'] = int(item.find('playcount').text) > 0 | |
171 | ||
172 | # genre can be multiple items, join by comma | |
173 | details['genre'] = ', '.join(n.text for n in item.findall('genre')) | |
174 | ||
5411d3e0 ER |
175 | # build text for 'cast' field |
176 | cast = [] | |
177 | for actor in item.findall('actor'): | |
178 | cast.append('%s as %s' % (actor.findtext('name'), actor.findtext('role'))) | |
179 | ||
180 | if cast: | |
181 | details['cast'] = "\n".join(cast) | |
182 | ||
91cfb43d ER |
183 | # put rest of information into notes field |
184 | notes = [] | |
185 | for k,v in self.notes_map.items(): | |
186 | v = item.findtext(v) | |
187 | if v: | |
188 | notes.append('%s: %s' % (k, v)) | |
189 | ||
190 | # credits can have multiple values, handle separately | |
191 | credits = ', '.join(n.text for n in item.findall('credits')) | |
192 | if credits: | |
193 | notes.append(_('Credits: %s') % credits) | |
194 | ||
195 | if notes: | |
196 | details['notes'] = "\n".join(notes) | |
197 | ||
78f08559 ER |
198 | # handle poster |
199 | # take first <thumb aspect="poster"> element | |
200 | posters = item.xpath('thumb[@aspect="poster"]') | |
201 | if posters: | |
0a532283 ER |
202 | self.poster_file = self.grab_url(posters[0].get('preview'), prefix = 'poster_', suffix = '.jpg') |
203 | details['image'] = self.poster_file | |
78f08559 | 204 | |
489423d6 ER |
205 | # increment for next iteration |
206 | self.itemindex = self.itemindex + 1 | |
207 | ||
208 | return details | |
0a532283 ER |
209 | |
210 | # grab url, return temp filename with remote file contents | |
211 | # XXX could not figure out how to use griffith own downloader with ui interaction, etc | |
212 | # XXX: grabbing urls while processing import xml blocks the ui | |
213 | def grab_url(self, url, prefix = None, suffix=None): | |
214 | log.debug("Downloading: %s" % url) | |
215 | (fd, local_file) = tempfile.mkstemp(suffix=suffix, prefix=prefix) | |
216 | try: | |
217 | f = urlopen(url) | |
218 | os.write(fd, f.read()) | |
219 | os.close(fd) | |
220 | ||
221 | except HTTPError, e: | |
222 | log.error("HTTP Error: %s: %s" % (e.code, url)) | |
223 | return None | |
224 | except URLError, e: | |
225 | log.error("URL Error: %s: %s" % (e.reason, url)) | |
226 | return None | |
227 | else: | |
228 | return local_file | |
229 | ||
230 | # we get here with an exception, cleanup and return None | |
231 | os.unlink(local_file) | |
232 | return None |