8 from urllib.error import HTTPError
12 gi.require_version('Gtk', '3.0')
13 gi.require_version('Gdk', '3.0')
14 from gi.repository import Gtk, Gdk
15 except (ImportError, ValueError) as err:
16 raise ImportError("""\
17 This program requires the Py G Object and GTK 3 packages; see the readme file""") \
27 def del_window(widget=None):
34 message = Gtk.MessageDialog(
36 message_type=Gtk.MessageType.ERROR,
37 buttons=Gtk.ButtonsType.CLOSE)
38 message.set_markup(markup)
43 def __init__(self, parent, size):
47 self.resumable = False
49 # Clear any "Download failed" message from previous run
50 self.parent.progress.set_text('')
51 self.parent.progress.set_show_text(True)
53 def set_fraction(self, p):
54 self.size = None # Stop calculating fraction based on size
57 def show_fraction(self, p):
59 self.parent.progress.set_fraction(p)
60 self.parent.set_title("({:.1%}) {}".format(p, self.parent.my_title))
63 def set_size(self, size):
64 size = '{:.1f} MB'.format(size / 1e6)
66 self.parent.labels[2][1].set_text(size)
69 self.show_fraction(size / self.size)
71 def done(self, stopped=False, failed=False):
74 if not stopped and not failed:
75 self.parent.progress.set_fraction(1.)
76 self.parent.progress.set_text('Download finished')
77 self.parent.set_title(self.parent.my_title)
78 self.parent.close_btn.set_label(Gtk.STOCK_CLOSE)
79 self.parent.pause_btn.hide()
80 self.parent.resume_btn.hide()
83 self.parent.progress.set_text('Download failed')
84 self.parent.pause_btn.hide()
86 label = 'Resume' if self.resumable else 'Retry'
87 self.parent.resume_btn.set_label(label)
88 self.parent.resume_btn.show()
92 class Downloader(Gtk.Window):
93 def __init__(self, target, title=None, dest_file=None):
95 self.dest_file = dest_file
97 Gtk.Window.__init__(self)
98 self.connect('destroy', del_window)
99 self.connect('destroy', self.on_destroy)
103 title = target['url']
104 self.my_title = dest_file.split("/")[-1]
105 self.set_title(title)
106 self.set_resizable(False)
107 self.set_default_size(400,0)
108 self.set_border_width(10)
113 table = Gtk.Table(3, 3, homogeneous=False)
117 label_term = Gtk.Label()
118 label_term.set_alignment(0., 0.5)
119 label_desc = Gtk.Label()
120 label_desc.set_alignment(1., 0.5)
121 self.labels.append([label_term, label_desc])
123 table.attach(label_term, 0,1, i,i+1, xpadding=xpadding, ypadding=ypadding)
124 table.attach(label_desc, 1,2, i,i+1, xpadding=xpadding, ypadding=ypadding)
126 self.labels[0][0].set_text('Name')
127 self.labels[0][1].set_text(title)
128 self.labels[1][0].set_text('Filename')
129 self.labels[1][1].set_text(dest_file.split('/')[-1])
130 self.labels[2][0].set_text('Download size')
131 self.labels[2][1].set_text('0.0 MB')
133 self.progress = Gtk.ProgressBar()
134 table.attach(self.progress, 0,2, 3,4, xpadding=xpadding, ypadding=8)
136 bb = Gtk.HButtonBox()
137 bb.set_layout(Gtk.ButtonBoxStyle.END)
138 self.pause_btn = Gtk.Button('Pause')
139 self.pause_btn.connect('clicked', self.pause_download)
140 self.resume_btn = Gtk.Button('Resume')
141 self.resume_btn.connect('clicked', self.start_download)
142 self.close_btn = Gtk.Button(stock=Gtk.STOCK_STOP)
143 self.close_btn.connect('clicked', self.destroy)
144 bb.pack_end(self.pause_btn, True, True, 0)
145 bb.pack_end(self.resume_btn, True, True, 0)
146 bb.pack_end(self.close_btn, True, True, 0)
149 vbox.pack_start(table, False, True, 0)
150 vbox.pack_start(bb, False, True, 0)
154 self.start_download() # kick off the DownloadWorker thread
156 def start_download(self, widget=None):
157 self.resume_btn.hide()
159 frontend = Frontend(self, self.target['size'])
160 self.pause_btn.hide()
161 self.job = iview.fetch.fetch_program(
163 dest_file=self.dest_file,
168 message = Gtk.MessageDialog(
170 type=Gtk.MessageType.ERROR,
171 buttons=Gtk.ButtonsType.CLOSE)
172 message.set_markup('<big><b>Download backend failed</b></big>\n\n' \
173 'Either the download backend in question failed for some reason, or one could not be found with which to download iView programmes. Please check the README.md file for instructions on setting this up.')
178 if frontend.resumable:
179 self.pause_btn.show()
182 def pause_download(self, widget=None):
185 def on_destroy(self, widget=None):
188 def destroy(self, null_param=None):
189 """Allow destroy() to be called with a parameter, thus allowing it to
190 be attached to the "clicked" event of a button.
192 Gtk.Window.destroy(self)
194 def on_download_clicked(widget, data=None):
195 global window, save_location
197 model, selected_iter = listing.get_selection().get_selected()
198 if selected_iter is None:
200 item = model[selected_iter]
201 if item is None or 'id' in item[1]: # Series, or loading placeholder
204 save_dialog = Gtk.FileChooserDialog('Save Video',
206 action=Gtk.FileChooserAction.SAVE,
207 buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
208 Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
210 resumable = iview.fetch.is_resumable(url)
211 save_dialog.set_do_overwrite_confirmation(not resumable)
213 if not resumable and save_location is None:
214 home = os.path.expanduser("~")
217 if save_location is not None:
218 save_dialog.set_current_folder(save_location)
220 # build a nice filename
221 # could use this ugly version instead:
222 # filename = iview.fetch.get_filename(item[1]['url'])
223 # TODO: This may hamper (future) GTK+-based subtitle downloading unless
224 # this is made into a shared function
226 program = model.get_value(model.iter_parent(selected_iter), 0)
228 filename = iview.fetch.descriptive_filename(program, title, url)
229 if not resumable and save_location is not None:
230 # Method to increment file names inside brackets
232 [root, ext] = os.path.splitext(filename)
233 # increment numbers until the file doesn't exist
234 while os.path.exists(os.path.join(save_location, filename)):
235 filename = "{} ({}){}".format(root, count, ext)
237 save_dialog.set_current_name(filename)
239 save_dialog.set_local_only(False) # allow saving to, e.g., SFTP
240 save_response = save_dialog.run()
242 if save_response == Gtk.ResponseType.OK:
243 dest_file = save_dialog.get_filename()
244 new_location = save_dialog.get_current_folder()
245 if new_location is not None:
246 save_location = new_location
247 Downloader(item[1], item[0], dest_file)
250 save_dialog.destroy()
252 def on_listing_cursor_changed(selection):
255 description.set_text('')
256 download_btn.set_sensitive(False)
258 model, selected_iter = selection.get_selected()
259 if selected_iter is None:
261 item = model.get_value(selected_iter, 1)
262 if item is None or 'id' in item: # Series or unloaded episode
265 description.set_text(item['description'])
266 download_btn.set_sensitive(True)
268 def load_programme():
271 for series in iview.comm.get_index():
272 item = [series['title'], dict(id=series['id'])]
273 series_iter = programme.append(None, item)
274 programme.append(series_iter, ['Loading...', None])
276 def load_series_items(widget, iter, path):
277 model = widget.get_model()
278 child = model.iter_children(iter)
280 if model.get_value(child, 1) is not None:
281 # This is not a "Loading..." item, so we've already fetched this.
285 series_id = model.get_value(iter, 1)['id']
286 items = iview.comm.get_series_items(series_id)
291 description=item['description'],
292 size=item.get('size'),
294 model.append(iter, [item['title'], target])
298 def about(widget, data=None):
299 d = Gtk.AboutDialog()
301 d.set_version(iview.config.version)
303 'Copyright \N{COPYRIGHT SIGN} 2009-2010 by Jeremy Visser')
304 d.set_license("""This program is free software: you can redistribute it and/or modify
305 it under the terms of the GNU General Public License as published by
306 the Free Software Foundation, either version 3 of the License, or
307 (at your option) any later version.
309 This program is distributed in the hope that it will be useful,
310 but WITHOUT ANY WARRANTY; without even the implied warranty of
311 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
312 GNU General Public License for more details.
314 You should have received a copy of the GNU General Public License
315 along with this program. If not, see <http://www.gnu.org/licenses/>.""")
321 # Seems to be necessary for other threads to be scheduled properly.
323 # http://stackoverflow.com/questions/8120860/python-doing-some-work-on-background-with-gtk-gui
324 from gi.repository import GLib
330 window = Gtk.Window()
331 window.set_title('iView')
332 window.set_default_size(400,450)
333 window.set_border_width(5)
334 window.connect('destroy', del_window)
339 programme_label = Gtk.Label()
340 programme_label.set_markup('<big><b>iView Programme</b></big>')
342 vbox.pack_start(programme_label, False, True, 0)
344 listing_scroller = Gtk.ScrolledWindow()
345 listing_scroller.set_policy(
346 Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
347 listing_scroller.set_shadow_type(Gtk.ShadowType.IN)
349 # Columns: 0=name, 1=target.
350 # Target has 'id' key only for series items, and
351 # is None for the unloaded episode marker.
353 programme = Gtk.TreeStore(str, object)
356 listing = Gtk.TreeView(programme)
357 listing.set_headers_visible(False)
358 selection = listing.get_selection()
359 selection.set_mode(Gtk.SelectionMode.SINGLE)
360 selection.connect('changed', on_listing_cursor_changed)
361 listing.connect('row-expanded', load_series_items)
363 tvcolumn = Gtk.TreeViewColumn('Program Name')
364 listing.append_column(tvcolumn)
365 cell = Gtk.CellRendererText()
366 tvcolumn.pack_start(cell, True)
367 tvcolumn.add_attribute(cell, 'text', 0)
369 listing_scroller.set_border_width(5)
371 listing_scroller.add(listing)
372 vbox.pack_start(listing_scroller, True, True, 0)
375 description = Gtk.Label()
376 description.set_line_wrap(True)
377 vbox.pack_start(description, False, True, 0)
379 bb = Gtk.HButtonBox()
380 bb.set_layout(Gtk.ButtonBoxStyle.EDGE)
381 bb.set_border_width(5)
383 about_btn = Gtk.Button(stock=Gtk.STOCK_ABOUT)
384 about_btn.connect('clicked', about)
387 download_btn = Gtk.Button('Download')
388 download_btn.set_sensitive(False)
389 download_btn.connect('clicked', on_download_clicked)
391 bb.pack_start(about_btn, True, True, 0)
392 bb.pack_start(download_btn, True, True, 0)
394 vbox.pack_start(bb, False, True, 0)
398 if len(sys.argv) >= 2 and sys.argv[1] in ('-c', '--cache'):
399 iview.config.cache = sys.argv[2]
402 iview.comm.get_config()
404 except HTTPError as error:
405 die('<big><b>Download failed</b></big>\n\n'
406 'Could not retrieve an important configuration file from iView.'
408 ' you are connected to the Internet.\n\n'
409 'If iView works fine in your web browser, then'
410 ' the iView API has most likely changed.'
411 ' Try and find an updated version of this program,'
412 ' or contact the author.\n\n'
413 'URL: {}'.format(error.url))
414 except EnvironmentError as error:
415 from traceback import format_exception_only, print_exc
417 die('<big><b>Failed getting programme list</b></big>\n\n' +
418 ''.join(format_exception_only(type(error), error)))
423 if __name__ == "__main__":