2
import gobject, gtk, gio, gconf
6
import xdg.BaseDirectory
10
from hash import hashFile
16
TOTEM_REMOTE_COMMAND_REPLACE = 14
28
# Map of the language codes used by opensubtitles.org's API to their human-readable name
29
LANGUAGES_STR = [(D_('iso_639_3', 'Albanian'), 'sq'),
30
(D_('iso_639_3', 'Arabic'), 'ar'),
31
(D_('iso_639_3', 'Armenian'), 'hy'),
32
(D_('iso_639_3', 'Neo-Aramaic, Assyrian'), 'ay'),
33
(D_('iso_639_3', 'Bosnian'), 'bs'),
34
(_('Brasilian Portuguese'), 'pb'),
35
(D_('iso_639_3', 'Bulgarian'), 'bg'),
36
(D_('iso_639_3', 'Catalan'), 'ca'),
37
(D_('iso_639_3', 'Chinese'), 'zh'),
38
(D_('iso_639_3', 'Croatian'), 'hr'),
39
(D_('iso_639_3', 'Czech'), 'cs'),
40
(D_('iso_639_3', 'Danish'), 'da'),
41
(D_('iso_639_3', 'Dutch'), 'nl'),
42
(D_('iso_639_3', 'English'), 'en'),
43
(D_('iso_639_3', 'Esperanto'), 'eo'),
44
(D_('iso_639_3', 'Estonian'), 'et'),
45
(D_('iso_639_3', 'Finnish'), 'fi'),
46
(D_('iso_639_3', 'French'), 'fr'),
47
(D_('iso_639_3', 'Galician'), 'gl'),
48
(D_('iso_639_3', 'Georgian'), 'ka'),
49
(D_('iso_639_3', 'German'), 'de'),
50
(D_('iso_639_3', 'Greek, Modern (1453-)'), 'el'),
51
(D_('iso_639_3', 'Hebrew'), 'he'),
52
(D_('iso_639_3', 'Hindi'), 'hi'),
53
(D_('iso_639_3', 'Hungarian'), 'hu'),
54
(D_('iso_639_3', 'Icelandic'), 'is'),
55
(D_('iso_639_3', 'Indonesian'), 'id'),
56
(D_('iso_639_3', 'Italian'), 'it'),
57
(D_('iso_639_3', 'Japanese'), 'ja'),
58
(D_('iso_639_3', 'Kazakh'), 'kk'),
59
(D_('iso_639_3', 'Korean'), 'ko'),
60
(D_('iso_639_3', 'Latvian'), 'lv'),
61
(D_('iso_639_3', 'Lithuanian'), 'lt'),
62
(D_('iso_639_3', 'Luxembourgish'), 'lb'),
63
(D_('iso_639_3', 'Macedonian'), 'mk'),
64
(D_('iso_639_3', 'Malay (macrolanguage)'), 'ms'),
65
(D_('iso_639_3', 'Norwegian'), 'no'),
66
(D_('iso_639_3', 'Occitan (post 1500)'), 'oc'),
67
(D_('iso_639_3', 'Persian'), 'fa'),
68
(D_('iso_639_3', 'Polish'), 'pl'),
69
(D_('iso_639_3', 'Portuguese'), 'pt'),
70
(D_('iso_639_3', 'Romanian'), 'ro'),
71
(D_('iso_639_3', 'Russian'), 'ru'),
72
(D_('iso_639_3', 'Serbian'), 'sr'),
73
(D_('iso_639_3', 'Slovak'), 'sk'),
74
(D_('iso_639_3', 'Slovenian'), 'sl'),
75
(D_('iso_639_3', 'Spanish'), 'es'),
76
(D_('iso_639_3', 'Swedish'), 'sv'),
77
(D_('iso_639_3', 'Thai'), 'th'),
78
(D_('iso_639_3', 'Turkish'), 'tr'),
79
(D_('iso_639_3', 'Ukrainian'), 'uk'),
80
(D_('iso_639_3', 'Vietnamese'), 'vi'),]
82
# Map of ISO 639-1 language codes to the codes used by opensubtitles.org's API
83
LANGUAGES = {'sq':'alb',
136
class SearchThread(threading.Thread):
138
This is the thread started when the dialog is searching for subtitles
140
def __init__(self, model):
143
self._lock = threading.Lock()
144
threading.Thread.__init__(self)
147
self.model.lock.acquire(True)
148
self.model.results = self.model.os_search_subtitles()
149
self.model.lock.release()
154
""" Thread-safe property to know whether the query is done or not """
155
self._lock.acquire(True)
160
class DownloadThread(threading.Thread):
162
This is the thread started when the dialog is downloading the subtitles.
164
def __init__(self, model, subtitle_id):
166
self.subtitle_id = subtitle_id
168
self._lock = threading.Lock()
169
threading.Thread.__init__(self)
172
self.model.lock.acquire(True)
173
self.model.subtitles = self.model.os_download_subtitles(self.subtitle_id)
174
self.model.lock.release()
179
""" Thread-safe property to know whether the query is done or not """
180
self._lock.acquire(True)
185
# OpenSubtitles.org API abstraction
187
class OpenSubtitlesModel(object):
189
This contains the logic of the opensubtitles service.
191
def __init__(self, server):
197
self.lang = LANGUAGES[locale.getlocale()[0].split('_')[0]]
203
self.lock = threading.Lock()
209
def os_login(self, username='', password=''):
211
Logs into the opensubtitles web service and gets a valid token for
212
the comming comunications. If we are already logged it only checks
213
the if the token is still valid.
221
# We have already logged-in before, check the connection
223
result = self.server.NoOperation(self.token)
226
if result and result['status'] != OK200:
229
result = self.server.LogIn(username, password, self.lang, USER_AGENT)
232
if result and result.get('status') == OK200:
233
self.token = result.get('token')
237
self.message = _('Could not contact the OpenSubtitles website')
241
def os_search_subtitles(self):
247
searchdata = {'sublanguageid': self.lang,
248
'moviehash' : self.hash,
249
'moviebytesize': str(self.size)}
251
result = self.server.SearchSubtitles(self.token, [searchdata])
252
except xmlrpclib.ProtocolError:
253
self.message = _('Could not contact the OpenSubtitles website')
255
if result.get('data'):
256
return result['data']
258
self.message = _('No results found')
262
def os_download_subtitles(self, subtitleId):
268
result = self.server.DownloadSubtitles(self.token, [subtitleId])
269
except xmlrpclib.ProtocolError:
270
self.message = _('Could not contact the OpenSubtitles website')
272
if result and result.get('status') == OK200:
274
subtitle64 = result['data'][0]['data']
276
self.message = _('Could not contact the OpenSubtitles website')
279
import StringIO, gzip, base64
280
subtitleDecoded = base64.decodestring(subtitle64)
281
subtitleGzipped = StringIO.StringIO(subtitleDecoded)
282
subtitleGzippedFile = gzip.GzipFile(fileobj=subtitleGzipped)
284
return subtitleGzippedFile.read()
289
class OpenSubtitles(totem.Plugin):
291
totem.Plugin.__init__(self)
293
self.gconf_client = gconf.client_get_default()
294
self.GCONF_BASE_DIR = "/apps/totem/plugins/opensubtitles/"
295
self.GCONF_LANGUAGE = "language"
297
# totem.Plugin methods
299
def activate(self, totem_object):
301
Called when the plugin is activated.
302
Here the sidebar page is initialized(set up the treeview, connect
303
the callbacks, ...) and added to totem.
306
@type totem_object: {totem.TotemObject}
308
self.totem = totem_object
311
self.manager = self.totem.get_ui_manager()
312
self.os_append_menu()
314
self.totem.connect('file-opened', self.on_totem__file_opened)
315
self.totem.connect('file-closed', self.on_totem__file_closed)
317
# Obtain the ServerProxy and init the model
318
server = xmlrpclib.Server('http://www.opensubtitles.org/xml-rpc')
319
self.model = OpenSubtitlesModel(server)
321
def deactivate(self, totem):
323
self.dialog.destroy()
326
self.os_delete_menu()
330
def os_build_dialog(self, action, totem_object):
331
builder = self.load_interface("opensubtitles.ui",
333
self.totem.get_main_window(),
336
# Obtain all the widgets we need to initialize
337
combobox = builder.get_object('language_combobox')
338
languages = builder.get_object('language_model')
339
self.progress = builder.get_object('progress_bar')
340
self.treeview = builder.get_object('subtitle_treeview')
341
self.liststore = builder.get_object('subtitle_model')
342
self.dialog = builder.get_object('subtitles_dialog')
343
self.find_button = builder.get_object('find_button')
344
self.apply_button = builder.get_object('apply_button')
345
self.close_button = builder.get_object('close_button')
347
# Set up and populate the languages combobox
348
renderer = gtk.CellRendererText()
349
sorted_languages = gtk.TreeModelSort(languages)
350
sorted_languages.set_sort_column_id(0, gtk.SORT_ASCENDING)
351
combobox.set_model(sorted_languages)
352
combobox.pack_start(renderer, True)
353
combobox.add_attribute(renderer, 'text', 0)
355
self.model.lang = self.gconf_get_str (self.GCONF_BASE_DIR + self.GCONF_LANGUAGE, self.model.lang)
356
for lang in LANGUAGES_STR:
357
it = languages.append(lang)
358
if LANGUAGES[lang[1]] == self.model.lang:
359
parentit = sorted_languages.convert_child_iter_to_iter (None, it)
360
combobox.set_active_iter(parentit)
362
# Set up the results treeview
363
renderer = gtk.CellRendererText()
364
self.treeview.set_model(self.liststore)
365
self.treeview.set_headers_visible(False)
366
self.treeview.insert_column_with_attributes(0, _("Subtitles"), renderer, text=0)
367
# translators comment:
368
# This is the file-type of the subtitle file detected
369
self.treeview.insert_column_with_attributes(1, _("Format"), renderer, text=1)
370
# translators comment:
371
# This is a rating of the quality of the subtitle
372
self.treeview.insert_column_with_attributes(2, _("Rating"), renderer, text=2)
374
self.apply_button.set_sensitive(False)
376
self.apply_button.connect('clicked', self.on_apply_clicked)
377
self.find_button.connect('clicked', self.on_find_clicked)
378
self.close_button.connect('clicked', self.on_close_clicked)
382
combobox_changed_id = combobox.connect('changed', self.on_combobox__changed)
383
self.dialog.connect ('delete-event', self.dialog.hide_on_delete)
384
self.dialog.set_transient_for (self.totem.get_main_window())
385
self.dialog.set_position (gtk.WIN_POS_CENTER_ON_PARENT)
387
# Connect the callback
388
self.treeview.get_selection().connect('changed', self.on_treeview__row_change)
389
self.treeview.connect('row-activated', self.on_treeview__row_activate)
391
def os_show_dialog(self, action, totem_object):
393
self.os_build_dialog(action, totem_object)
395
filename = self.totem.get_current_mrl()
396
if not self.model.results or filename != self.filename:
397
self.filename = filename
399
self.dialog.show_all()
401
self.progress.set_fraction(0.0)
403
def os_append_menu(self):
407
self.os_action_group = gtk.ActionGroup('OpenSubtitles')
409
self.action = gtk.Action('opensubtitles',
410
_('_Download Movie Subtitles...'),
411
_("Download movie subtitles from OpenSubtitles"),
414
self.os_action_group.add_action(self.action)
416
self.manager.insert_action_group(self.os_action_group, 0)
418
self.menu_id = self.manager.new_merge_id()
419
self.manager.add_ui(self.menu_id,
420
'/tmw-menubar/view/subtitle-download-placeholder',
423
gtk.UI_MANAGER_MENUITEM,
426
self.action.set_visible(True)
428
self.manager.ensure_update()
430
self.action.connect('activate', self.os_show_dialog, self.totem)
432
self.action.set_sensitive(self.totem.is_playing() and
433
self.os_check_allowed_scheme() and
434
not self.os_check_is_audio())
436
def os_check_allowed_scheme(self):
437
scheme = gio.File(self.totem.get_current_mrl()).get_uri_scheme()
438
if scheme == 'dvd' or scheme == 'http' or scheme == 'dvb' or scheme == 'vcd':
442
def os_check_is_audio(self):
443
# FIXME need to use something else here
444
# I think we must use video widget metadata but I don't found a way
445
# to get this info from python
446
filename = self.totem.get_current_mrl()
447
if gio.content_type_guess(filename).split('/')[0] == 'audio':
451
def os_delete_menu(self):
452
self.manager.remove_action_group(self.os_action_group)
453
self.manager.remove_ui(self.menu_id)
455
def os_get_results(self):
458
self.liststore.clear()
459
self.treeview.set_headers_visible(False)
460
self.model.results = []
461
self.apply_button.set_sensitive(False)
462
self.find_button.set_sensitive(False)
464
self.dialog.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
466
thread = SearchThread(self.model)
468
gobject.idle_add(self.os_populate_treeview)
470
self.progress.set_text(_('Searching subtitles...'))
471
gobject.timeout_add(350, self.os_progress_bar_increment, thread)
473
def os_populate_treeview(self):
476
if self.model.lock.acquire(False) == False:
479
if self.model.results:
480
self.apply_button.set_sensitive(True)
481
for subData in self.model.results:
482
if not SUBTITLES_EXT.count(subData['SubFormat']):
484
self.liststore.append([subData['SubFileName'], subData['SubFormat'], subData['SubRating'], subData['IDSubtitleFile'],])
485
self.treeview.set_headers_visible(True)
487
self.apply_button.set_sensitive(False)
489
self.model.lock.release()
491
self.dialog.window.set_cursor(None)
495
def os_save_selected_subtitle(self, filename=None):
498
self.dialog.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
500
model, rows = self.treeview.get_selection().get_selected_rows()
502
iter = model.get_iter(rows[0])
503
subtitle_id = model.get_value(iter, 3)
504
subtitle_format = model.get_value(iter, 1)
509
directory = gio.File(xdg.BaseDirectory.xdg_cache_home + sep + 'totem' + sep + 'subtitles' + sep)
510
if not directory.query_exists():
511
directory.make_directory_with_parents()
513
file = gio.File(self.filename)
514
movie_name = file.get_basename().rpartition('.')[0]
515
filename = directory.get_uri() + sep + movie_name + '.' + subtitle_format
517
self.model.subtitles = ''
519
thread = DownloadThread(self.model, subtitle_id)
521
gobject.idle_add(self.os_save_subtitles, filename)
523
self.progress.set_text(_('Downloading the subtitles...'))
524
gobject.timeout_add(350, self.os_progress_bar_increment, thread)
529
def os_save_subtitles(self, filename):
530
if self.model.lock.acquire(False) == False:
533
if self.model.subtitles:
534
# Delete all previous cached subtitle for this file
535
for ext in SUBTITLES_EXT:
536
fp = gio.File(filename[:-3] + ext)
537
if fp.query_exists():
540
fp = gio.File(filename)
541
suburi = fp.get_uri ()
543
subFile = fp.replace('', False)
544
subFile.write(self.model.subtitles)
547
self.model.lock.release()
549
self.dialog.window.set_cursor(None)
552
self.totem.set_current_subtitle(suburi)
556
def os_progress_bar_increment(self, thread):
559
self.progress.pulse()
562
if self.model.message:
563
self.progress.set_text(self.model.message)
565
self.progress.set_text('')
567
self.progress.set_fraction(0.0)
568
self.find_button.set_sensitive(True)
569
self.apply_button.set_sensitive(False)
570
self.treeview.set_sensitive(True)
573
def os_download_and_apply(self):
574
self.apply_button.set_sensitive(False)
575
self.find_button.set_sensitive(False)
576
self.action.set_sensitive(False)
577
self.treeview.set_sensitive(False)
578
self.os_save_selected_subtitle()
582
def on_treeview__row_change(self, selection):
583
if selection.count_selected_rows() > 0:
584
self.apply_button.set_sensitive(True)
586
self.apply_button.set_sensitive(False)
588
def on_treeview__row_activate(self, path, column, data):
589
self.os_download_and_apply()
591
def on_totem__file_opened(self, totem, filename):
594
# Check if allows subtitles
595
if self.os_check_allowed_scheme() and not self.os_check_is_audio():
596
self.action.set_sensitive(True)
598
self.find_button.set_sensitive(True)
599
self.filename = self.totem.get_current_mrl()
600
self.liststore.clear()
601
self.treeview.set_headers_visible(False)
602
self.apply_button.set_sensitive(False)
605
self.action.set_sensitive(False)
606
if self.dialog and self.dialog.is_active():
607
self.liststore.clear()
608
self.treeview.set_headers_visible(False)
609
self.apply_button.set_sensitive(False)
610
self.find_button.set_sensitive(False)
612
def on_totem__file_closed(self, totem):
613
self.action.set_sensitive(False)
615
self.apply_button.set_sensitive(False)
616
self.find_button.set_sensitive(False)
618
def on_combobox__changed(self, combobox):
619
iter = combobox.get_active_iter()
620
self.model.lang = LANGUAGES[combobox.get_model().get_value(iter, 1)]
621
self.gconf_set_str(self.GCONF_BASE_DIR + self.GCONF_LANGUAGE,
624
def on_close_clicked(self, data):
625
self.dialog.destroy()
628
def on_apply_clicked(self, data):
629
self.os_download_and_apply()
631
def on_find_clicked(self, data):
632
self.apply_button.set_sensitive(False)
633
self.find_button.set_sensitive(False)
634
self.filename = self.totem.get_current_mrl()
635
self.model.hash , self.model.size = hashFile(self.filename)
637
self.os_get_results()
639
def gconf_get_str(self, key, default = ""):
640
val = self.gconf_client.get(key)
642
if val is not None and val.type == gconf.VALUE_STRING:
643
return val.get_string()
647
def gconf_set_str(self, key, val):
648
self.gconf_client.set_string(key, val)