diff --git a/pitivi/ui/mainwindow.py b/pitivi/ui/mainwindow.py index 8b2cf54e..efbb2215 100644 --- a/pitivi/ui/mainwindow.py +++ b/pitivi/ui/mainwindow.py @@ -33,6 +33,19 @@ from urllib import unquote import webbrowser +try: + import gconf +except: + HAVE_GCONF = False +else: + HAVE_GCONF = True + +try: + import gdata.youtube.client + HAVE_GDATA_2 = True +except: + HAVE_GDATA_2 = False + from gettext import gettext as _ from gtk import RecentManager @@ -744,7 +757,8 @@ def _projectManagerNewProjectLoadedCb(self, projectManager, project): self._connectToProjectSources(project.sources) can_render = project.timeline.duration > 0 self.render_button.set_sensitive(can_render) - self.publish_button.set_sensitive(can_render) + if HAVE_GDATA_2: + self.publish_button.set_sensitive(can_render) self._syncDoUndo(self.app.action_log) if self._missingUriOnLoading: @@ -1062,7 +1076,8 @@ def _timelineDurationChangedCb(self, timeline, duration): else: sensitive = False self.render_button.set_sensitive(sensitive) - self.publish_button.set_sensitive(sensitive) + if HAVE_GDATA_2: + self.publish_button.set_sensitive(sensitive) ## other diff --git a/pitivi/ui/publishtoyoutubedialog.glade b/pitivi/ui/publishtoyoutubedialog.glade index fffc66dd..933d1edd 100644 --- a/pitivi/ui/publishtoyoutubedialog.glade +++ b/pitivi/ui/publishtoyoutubedialog.glade @@ -18,6 +18,9 @@ + + + True @@ -180,7 +183,7 @@ True - 5 + 7 2 @@ -322,6 +325,42 @@ 5 + + + True + True + + + + 1 + 2 + 6 + 7 + + + + + True + Folder : + + + 5 + 6 + + + + + True + File name: + + + 6 + 7 + + + + + @@ -339,17 +378,19 @@ True - + + + + True - Initializing .. + False + False + end 0 - - - summary diff --git a/pitivi/ui/publishtoyoutubedialog.py b/pitivi/ui/publishtoyoutubedialog.py index c6a04c1f..70f7655d 100644 --- a/pitivi/ui/publishtoyoutubedialog.py +++ b/pitivi/ui/publishtoyoutubedialog.py @@ -2,7 +2,7 @@ # # ui/publishtoyoutubedialog.py # -# Copyright (c) 2010, Magnus Hoff +# Copyright (c) 2010, Mathieu Duponchelle # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -23,16 +23,18 @@ Dialog for publishing to YouTube """ -import tempfile, os, gtk +import gtk +import time +import thread +from gst import SECOND from pitivi.log.loggable import Loggable from pitivi.ui.glade import GladeWindow from pitivi.actioner import Renderer -from projectsettings import ProjectSettingsDialog -from pitivi.youtube_glib import AsyncYT, PipeWrapper +from pitivi.youtube_glib import YTUploader from gettext import gettext as _ -from gtk import ProgressBar from gobject import timeout_add from string import ascii_lowercase, ascii_uppercase, maketrans, translate +from pitivi.utils import beautify_length try : import gnomekeyring as gk unsecure_storing = False @@ -51,6 +53,7 @@ def __init__(self, app, project, pipeline=None): self.app = app self.pipeline = pipeline + self.project = project # UI widgets self.login = self.widgets["login"] @@ -79,48 +82,47 @@ def __init__(self, app, project, pipeline=None): self.password.set_text(item_info.get_secret()) gk.lock_sync('pitivi') - self.remember_me = False + self.uploader = YTUploader() self.description = self.widgets["description"] self.tags = self.widgets["tags"] self.categories = gtk.combo_box_new_text() self.widgets["table2"].attach(self.categories, 1, 2, 3, 4) self.categories.show() self.categories.set_title("Choose a category") - self.hbox = gtk.HBox() - self.progressbar = gtk.ProgressBar() + for e in catlist: + self.categories.append_text(e) + + self.renderbar = self.widgets["renderbar"] + self.uploadbar = gtk.ProgressBar() self.stopbutton = gtk.ToolButton(gtk.STOCK_CANCEL) - self.hbox.pack_start(self.progressbar) - self.hbox.pack_start(self.stopbutton) + self.hbox = gtk.HBox() + self.hbox.pack_start(self.uploadbar) + self.hbox.pack_end(self.stopbutton) self.taglist = [] + self.fileentry = self.widgets["fileentry"] + self.updateFilename(self.project.name) + self.filebutton = gtk.FileChooserButton("Select a file") + self.widgets["table2"].attach(self.filebutton, 1, 2, 5, 6) + self.filebutton.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.filebutton.set_current_folder(self.app.settings.lastExportFolder) + self.filebutton.show() + + self.remember_me = False + # Assistant pages self.login_page = self.window.get_nth_page(0) self.metadata_page = self.window.get_nth_page(1) self.render_page = self.window.get_nth_page(2) self.announce_page = self.window.get_nth_page(3) - for e in catlist: - self.categories.append_text(e) - self.description.get_buffer().connect("changed", self._descriptionChangedCb) self.categories.connect("changed", self._categoryChangedCb) self.stopbutton.connect('clicked', self._finishCb) self.mainquitsignal = self.app.connect('destroy', self._mainQuitCb) - + self.connect("eos", self._renderingDoneCb) self.window.connect("delete-event", self._deleteEventCb) - self.tmpdir = tempfile.mkdtemp() - self.fifoname = os.path.join(self.tmpdir, 'pitivi_rendering_fifo') - os.mkfifo(self.fifoname) - - # TODO: This is probably not the best way to build an URL - #self.outfile = 'file:/home/mag/test.webm' #'file:' + self.fifoname - outfile = 'file://' + self.fifoname - - Renderer.__init__(self, project, pipeline, outfile = outfile) - - # YouTube integration - self.yt = AsyncYT() self.metadata = { "title": "", "description": "", @@ -130,27 +132,8 @@ def __init__(self, app, project, pipeline=None): } self.has_started_rendering = False - def _shutDown(self): - self.debug("shutting down") - self.app.project.setSettings(self.oldsettings) - self.app.publish_button.set_sensitive(True) - self.app.handler_disconnect(self.mainquitsignal) - - try: - os.remove(self.fifoname) - except OSError: - pass - - try: - os.rmdir(self.tmpdir) - except OSError: - pass - - # Abort recording - self.removeAction() - self.yt.stop() - self.window.destroy() - self.destroy() + def updateFilename(self, name): + self.fileentry.set_text(name + ".avi") def _storePassword(self): if unsecure_storing: @@ -168,18 +151,51 @@ def _storePassword(self): a = gk.item_create_sync('pitivi', gk.ITEM_GENERIC_SECRET, self.username.get_text(), atts, self.password.get_text(), True) - def _mainQuitCb(self, ignored): - self.yt.stop() - self.window.destroy() - self.destroy() + def _update_metadata_page_complete(self): + is_complete = all([ + len(self.metadata["title"]) != 0, + len(self.metadata["description"]) != 0, + ]) + self.window.set_page_complete(self.metadata_page, is_complete) - def _deleteEventCb(self, window, event): - self.debug("delete event") - self._shutDown() + def _startRendering(self): - def _cancelCb(self, ignored): - self.debug("cancel event") - self._shutDown() + self.has_started_rendering = True + + # Start rendering: + self.filename = self.filebutton.get_uri() + "/" + self.fileentry.get_text() + Renderer.__init__(self, self.project, self.pipeline, outfile = + self.filename) + self.app.set_sensitive(False) + self.startAction() + self.renderbar.set_fraction(0) + self.visible = True + + def updatePosition(self, fraction, estimated, uploading = False): + if not uploading: + self.renderbar.set_fraction(fraction) + self.app.set_title(_("%d%% Rendered") % int(100 * fraction)) + else: + self.uploadbar.set_fraction(fraction) + if estimated and not uploading: + self.renderbar.set_text(_("About %s left in rendering") % estimated) + elif estimated and uploading: + self.uploadbar.set_text(_("About %s left in uploading") % estimated) + + def _shutDown(self): + self.debug("shutting down") + if self.uploader.uploader: + self.uploader.uploader.run = False + self.app.project.setSettings(self.oldsettings) + self.app.set_sensitive(True) + self.app.publish_button.set_sensitive(True) + self.app.handler_disconnect(self.mainquitsignal) + + # Abort recording + if self.has_started_rendering: + self.removeAction() + self.window.destroy() + self.destroy() def _rememberMeCb(self, button): self.remember_me = False @@ -190,16 +206,14 @@ def _loginClickedCb(self, *args): self.debug("login clicked") self.login_status.set_text("Logging in...") # TODO: This should activate a throbber - self.yt.authenticate_with_password(self.username.get_text(), self.password.get_text(), self._loginResultCb) + thread.start_new_thread (self.uploader.authenticate_with_password, (self.username.get_text(), + self.password.get_text(), self._loginResultCb)) def _loginResultCb(self, result): # TODO: The throbber should now be deactivated - status = result[0] - if status == 'good': - status, login_token = result + if result == 'good': if self.remember_me: self._storePassword() - self.login_status.set_text("Logged in") self.window.set_page_complete(self.login_page, True) self.window.set_current_page(self.window.get_current_page() + 1) else: @@ -207,13 +221,6 @@ def _loginResultCb(self, result): self.login_status.set_text(str(exception)) self.window.set_page_complete(self.login_page, False) - def _update_metadata_page_complete(self): - is_complete = all([ - len(self.metadata["title"]) != 0, - len(self.metadata["description"]) != 0, - ]) - self.window.set_page_complete(self.metadata_page, is_complete) - def _titleChangedCb(self, entry): self.metadata["title"] = entry.get_text() self._update_metadata_page_complete() @@ -246,67 +253,58 @@ def _newTagCb(self, entry): def _categoryChangedCb(self, combo): self.metadata["category"] = combo.get_active_text() - def _prepareCb(self, assistant, page): - if page == self.render_page and not self.has_started_rendering: - self._startRenderAndUpload() - - def _destroyCb (self, ignored): - self._shutDown() - def _changeStatusCb (self, button): if button.get_active(): self.metadata["private"] = True else: self.metadata["private"] = False - def _startRenderAndUpload(self): - - self.has_started_rendering = True - - # Start uploading: - self.yt.upload(lambda: PipeWrapper(open(self.fifoname, 'rb')), self.metadata, self._uploadDoneCb) + def _prepareCb(self, assistant, page): + if page == self.render_page and not self.has_started_rendering: + self._startRendering() - # Start rendering: - self.startAction() + def _renderingDoneCb(self, data): + self.app.set_sensitive(True) + self.app.set_title(_("PiTiVi")) + self.timestarted = time.time() + self.window.hide() self.app.sourcelist.pack_end(self.hbox, False, False) self.hbox.show_all() - self.progressbar.set_fraction(0) - self.visible = True + self.filename = self.filename.split("://")[1] + self.uploader.upload(self.filename, self.metadata, self._uploadProgressCb, self._uploadDoneCb) + + def _uploadProgressCb(self, done, total): + timediff = time.time() - self.timestarted + fraction = float(min(done, total)) / float(total) + if timediff > 3.0: + totaltime = (timediff * float(total) / float(done)) - timediff + text = beautify_length(int(totaltime * SECOND)) + self.updatePosition(fraction, text, uploading = True) + + def _uploadDoneCb(self, video_entry): + print "done !" + self.entry = gtk.Entry() + link = video_entry.find_html_link().split("&")[0] + self.entry.set_text(link) + self.uploadbar.destroy() + self.hbox.pack_start (self.entry) + self.entry.show() - def updatePosition(self, fraction, text): - self.progressbar.set_fraction(fraction) - if self.visible: - self.window.destroy() - self.visible = False - if text is not None and fraction < 0.99: - self.progressbar.set_text(_("About %s left") % text) - elif fraction < 0.05: - self.progressbar.set_text(_("Starting rendering")) - elif fraction > 0.99: - self.progressbar.set_text(_("Rendering done, finishing uploading")) - self.progressbar.set_pulse_step (0.05) - self.over = False - timeout_add (400, self._pulseCb) - - def _pulseCb(self): - self.progressbar.pulse() - if not self.over : - timeout_add (400, self._pulseCb) - return False + def _mainQuitCb(self, ignored): + self.window.destroy() + self.destroy() + + def _deleteEventCb(self, window, event): + self.debug("delete event") + self._shutDown() + + def _cancelCb(self, ignored): + self.debug("cancel event") + self._shutDown() + + def _destroyCb (self, ignored): + self._shutDown() def _finishCb(self, unused): self.hbox.destroy() self._shutDown() - - def _uploadDoneCb(self, result): - self.entry = gtk.Entry() - if result[0] == "good": - status, new_entry = result - self.entry.set_text(new_entry.GetSwfUrl().split ("?")[0]) - else: - status, exception = result - self.entry.set_text("error : " + status + exception) - self.over = True - self.progressbar.destroy() - self.hbox.pack_start (self.entry) - self.entry.show() diff --git a/pitivi/youtube_glib.py b/pitivi/youtube_glib.py index ae643a4f..56276e8f 100644 --- a/pitivi/youtube_glib.py +++ b/pitivi/youtube_glib.py @@ -2,7 +2,7 @@ # # youtube_glib.py # -# Copyright (c) 2010, Magnus Hoff +# Copyright (c) 2010, Mathieu Duponchelle # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -26,105 +26,93 @@ """ import gobject -import gdata.youtube, gdata.youtube.service -import threading -from Queue import Queue +import gdata.youtube +import gdata.youtube.client +import gdata.client +import gdata.youtube.data +from os.path import getsize +import thread + +APP_NAME = 'PiTiVi' +DEVELOPER_KEY = 'AI39si5DzhNX8NS0iEZl2Xg3uYj54QG57atp6v5w-FDikhMRYseN6MOtR8Bfvss4C0rTSqyJaTvgN8MHAszepFXz-zg4Zg3XNQ' +CREATE_SESSION_URI = '/resumable/feeds/api/users/default/uploads' +class ResumableYouTubeUploader(object): + def __init__(self, filepath, client): -CLIENT_ID = 'PiTiVi' -DEVELOPER_KEY = 'AI39si5DzhNX8NS0iEZl2Xg3uYj54QG57atp6v5w-FDikhMRYseN6MOtR8Bfvss4C0rTSqyJaTvgN8MHAszepFXz-zg4Zg3XNQ' + self.client = client + self.client.host = "uploads.gdata.youtube.com" -class PipeWrapper: - """Helper class to make gdata work with pipes""" - def __init__(self, f): - self._f = f - def read(self, *args): - return self._f.read(*args) - - -def upload(yt_service, metadata, filename): - text = metadata['category'] if metadata['category'] != None else 'Film' - print text - my_media_group = gdata.media.Group( - keywords = gdata.media.Keywords(text=metadata["tags"]), - title = gdata.media.Title(text=metadata["title"]), - description = gdata.media.Description(description_type='plain', text=metadata["description"]), - category = [ - gdata.media.Category( - text = text, - scheme = 'http://gdata.youtube.com/schemas/2007/categories.cat', - ), - ], - private = gdata.media.Private() if metadata["private"] else None, - ) - print metadata['category'] - video_entry = gdata.youtube.YouTubeVideoEntry( - media = my_media_group, - ) - new_entry = yt_service.InsertVideoEntry(video_entry, filename) - print "c'est fait petit" - return new_entry - - -class YTThread(threading.Thread): - def __init__(self, queue): - threading.Thread.__init__(self) - self._queue = queue - - def run(self): - self._yt_service = gdata.youtube.service.YouTubeService() - self._yt_service.source = CLIENT_ID - self._yt_service.developer_key = DEVELOPER_KEY - self._yt_service.client_id = CLIENT_ID - - self._running = True - while self._running: - task = self._queue.get() - task() - - def yt_stop(self): - self._running = False + self.f = open(filepath) + file_size = getsize(self.f.name) - def authenticate_with_password(self, username, password, callback): - try: - self._yt_service.email = username - self._yt_service.password = password - self._yt_service.ProgrammaticLogin() - gobject.idle_add(callback, ("good", self._yt_service.GetClientLoginToken())) - except Exception, e: - gobject.idle_add(callback, ("bad", e)) + self.uploader = gdata.client.ResumableUploader( + self.client, self.f, "video/avi", file_size, + chunk_size=1024*64, desired_class=gdata.youtube.data.VideoEntry) - def authenticate_with_token(self, token, callback): - self._yt_service.SetClientLoginToken(token) - gobject.idle_add(callback, token) + def __del__(self): + if self.uploader is not None: + self.uploader.file_handle.close() - def upload(self, filename, metadata, callback): - try: - new_entry = upload(self._yt_service, metadata, filename()) - gobject.idle_add(callback, ("good", new_entry)) - except Exception, e: - gobject.idle_add(callback, ("bad", e)) + def uploadInManualChunks(self, new_entry, on_chunk_complete, callback): + uri = CREATE_SESSION_URI + self.run = True -class AsyncYT: - def __init__(self): - self._queue = Queue() - self._yt_thread = YTThread(self._queue) - self._yt_thread.start() + self.uploader._InitSession(uri, entry=new_entry, headers={"X-GData-Key": "key=" + DEVELOPER_KEY, + "Slug" : None}) - def __del__(self): - """This is an absolute last resort. Please call stop() manually instead""" - self.stop() + start_byte = 0 + entry = None - def authenticate_with_password(self, username, password, callback): - self._queue.put(lambda: self._yt_thread.authenticate_with_password(username, password, callback)) + while not entry and self.run: + entry = self.uploader.UploadChunk(start_byte, self.uploader.file_handle.read(self.uploader.chunk_size)) + start_byte += self.uploader.chunk_size + on_chunk_complete(start_byte, self.uploader.total_file_size) + callback(entry) + + +class UploadBase(): + def __init__(self): + self.uploader = None - def authenticate_with_token(self, token, callback): - self._queue.put(lambda: self._yt_thread.authenticate_with_token(token, callback)) + def authenticate_with_password(self, username, password, callback): + pass def upload(self, filename, metadata, callback): - self._queue.put(lambda: self._yt_thread.upload(filename, metadata, callback)) + pass + +class YTUploader(UploadBase): + def __init__(self): + UploadBase.__init__(self) + + def authenticate_with_password(self, username, password, callback): + try: + self.client = gdata.youtube.client.YouTubeClient(source=APP_NAME) + self.client.ssl = False + self.client.http_client.debug = False + self.convert = None + self.client.ClientLogin(username, password, self.client.source) + gobject.idle_add(callback, ("good")) + except Exception, e: + gobject.idle_add(callback, ("bad", e)) - def stop(self): - self._queue.put(self._yt_thread.yt_stop) - self._yt_thread.join() + def upload(self, filename, metadata, progressCb, doneCb): + self.uploader = ResumableYouTubeUploader(filename, self.client) + + text = metadata['category'] if metadata['category'] != None else 'Film' + print text + my_media_group = gdata.media.Group( + keywords = gdata.media.Keywords(text=metadata["tags"]), + title = gdata.media.Title(text=metadata["title"]), + description = gdata.media.Description(description_type='plain', text=metadata["description"]), + category = [ + gdata.media.Category( + text = text, + scheme = 'http://gdata.youtube.com/schemas/2007/categories.cat', + ), + ], + private = gdata.media.Private() if metadata["private"] else None, + ) + new_entry = gdata.youtube.YouTubeVideoEntry(media=my_media_group) + thread.start_new_thread(self.uploader.uploadInManualChunks, (new_entry, progressCb, doneCb))