From 0ba0b153f9787ab64daead531eb0a71d1ecfbe14 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 8 Mar 2011 18:56:32 -0600 Subject: [PATCH] Adding voicemail support to the session and making it possible to save it --- src/backends/gv_backend.py | 5 +- src/backends/gvoice/gvoice.py | 7 +-- src/dialogs.py | 130 +++++++++++++++++++++++++++++++++++++++-- src/gv_views.py | 16 +++-- src/session.py | 67 ++++++++++++++++++++- 5 files changed, 202 insertions(+), 23 deletions(-) diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py index 2414049..a4fadde 100644 --- a/src/backends/gv_backend.py +++ b/src/backends/gv_backend.py @@ -103,15 +103,14 @@ class GVDialer(object): def get_feed(self, feed): return self._gvoice.get_feed(feed) - def download(self, messageId, adir): + def download(self, messageId, targetPath): """ Download a voicemail or recorded call MP3 matching the given ``msg`` which can either be a ``Message`` instance, or a SHA1 identifier. - Saves files to ``adir`` (defaults to current directory). Message hashes can be found in ``self.voicemail().messages`` for example. Returns location of saved file. """ - return self._gvoice.download(messageId, adir) + self._gvoice.download(messageId, targetPath) def is_valid_syntax(self, number): """ diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py index fda7f24..f73600e 100755 --- a/src/backends/gvoice/gvoice.py +++ b/src/backends/gvoice/gvoice.py @@ -418,20 +418,17 @@ class GVoiceBackend(object): url = self._downloadVoicemailURL+messageId return url - def download(self, messageId, adir): + def download(self, messageId, targetPath): """ Download a voicemail or recorded call MP3 matching the given ``msg`` which can either be a ``Message`` instance, or a SHA1 identifier. - Saves files to ``adir`` (defaults to current directory). Message hashes can be found in ``self.voicemail().messages`` for example. @returns location of saved file. @blocks """ page = self._get_page(self.recording_url(messageId)) - fn = os.path.join(adir, '%s.mp3' % messageId) - with open(fn, 'wb') as fo: + with open(targetPath, 'wb') as fo: fo.write(page) - return fn def is_valid_syntax(self, number): """ diff --git a/src/dialogs.py b/src/dialogs.py index be4a177..7894103 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -533,6 +533,120 @@ class ContactList(object): self._session.draft.remove_contact(self._uiItems[index]["cid"]) +class VoicemailPlayer(object): + + def __init__(self, app, session, errorLog): + self._app = app + self._session = session + self._errorLog = errorLog + self._session.voicemailAvailable.connect(self._on_voicemail_downloaded) + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) + + self._downloadButton = QtGui.QPushButton("Download Voicemail") + self._downloadButton.clicked.connect(self._on_voicemail_download) + self._downloadLayout = QtGui.QHBoxLayout() + self._downloadLayout.addWidget(self._downloadButton) + self._downloadWidget = QtGui.QWidget() + self._downloadWidget.setLayout(self._downloadLayout) + + self._playLabel = QtGui.QLabel("Voicemail") + self._saveButton = QtGui.QPushButton("Save") + self._saveButton.clicked.connect(self._on_voicemail_save) + self._playerLayout = QtGui.QHBoxLayout() + self._playerLayout.addWidget(self._playLabel) + self._playerLayout.addWidget(self._saveButton) + self._playerWidget = QtGui.QWidget() + self._playerWidget.setLayout(self._playerLayout) + + self._visibleWidget = None + self._layout = QtGui.QHBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self._widget = QtGui.QWidget() + self._widget.setLayout(self._layout) + self._update_state() + + @property + def toplevel(self): + return self._widget + + def destroy(self): + self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded) + self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed) + + def _show_download(self, messageId): + if self._visibleWidget is self._downloadWidget: + return + self._hide() + self._layout.addWidget(self._downloadWidget) + self._visibleWidget = self._downloadWidget + self._visibleWidget.show() + + def _show_player(self, messageId): + if self._visibleWidget is self._playerWidget: + return + self._hide() + self._layout.addWidget(self._playerWidget) + self._visibleWidget = self._playerWidget + self._visibleWidget.show() + + def _hide(self): + if self._visibleWidget is None: + return + self._visibleWidget.hide() + self._layout.removeWidget(self._visibleWidget) + self._visibleWidget = None + + def _update_state(self): + if self._session.draft.get_num_contacts() != 1: + self._hide() + return + + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + if messageId is None: + self._hide() + return + + if self._session.is_available(messageId): + self._show_player(messageId) + else: + self._show_download(messageId) + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_save(self, arg): + with qui_utils.notify_error(self._app.errorLog): + targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)") + targetPath = unicode(targetPath) + if not targetPath: + return + + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + sourcePath = self._session.voicemail_path(messageId) + import shutil + shutil.copy2(sourcePath, targetPath) + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_download(self, arg): + with qui_utils.notify_error(self._app.errorLog): + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + self._session.download_voicemail(messageId) + self._hide() + + @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + with qui_utils.notify_error(self._app.errorLog): + self._update_state() + + @QtCore.pyqtSlot(str, str) + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_downloaded(self, messageId, filepath): + with qui_utils.notify_error(self._app.errorLog): + self._update_state() + + class SMSEntryWindow(qwrappers.WindowWrapper): MAX_CHAR = 160 @@ -562,12 +676,14 @@ class SMSEntryWindow(qwrappers.WindowWrapper): self._history = QtGui.QLabel() self._history.setTextFormat(QtCore.Qt.RichText) self._history.setWordWrap(True) + self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog) self._smsEntry = QtGui.QTextEdit() self._smsEntry.textChanged.connect(self._on_letter_count_changed) self._entryLayout = QtGui.QVBoxLayout() self._entryLayout.addWidget(self._targetList.toplevel) self._entryLayout.addWidget(self._history) + self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0) self._entryLayout.addWidget(self._smsEntry) self._entryLayout.setContentsMargins(0, 0, 0, 0) self._entryWidget = QtGui.QWidget() @@ -646,6 +762,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper): self._session.draft.called.disconnect(self._on_op_finished) self._session.draft.cancelled.disconnect(self._on_op_finished) self._session.draft.error.disconnect(self._on_op_error) + self._voicemailPlayer.destroy() window = self._window self._window = None try: @@ -803,12 +920,13 @@ class SMSEntryWindow(qwrappers.WindowWrapper): @QtCore.pyqtSlot() @misc_utils.log_exception(_moduleLogger) def _on_refresh_history(self): - draftContactsCount = self._session.draft.get_num_contacts() - if draftContactsCount != 1: - # Changing contact count will automatically refresh it - return - (cid, ) = self._session.draft.get_contacts() - self._update_history(cid) + with qui_utils.notify_error(self._app.errorLog): + draftContactsCount = self._session.draft.get_num_contacts() + if draftContactsCount != 1: + # Changing contact count will automatically refresh it + return + (cid, ) = self._session.draft.get_contacts() + self._update_history(cid) @QtCore.pyqtSlot() @misc_utils.log_exception(_moduleLogger) diff --git a/src/gv_views.py b/src/gv_views.py index 08de9b7..18251df 100644 --- a/src/gv_views.py +++ b/src/gv_views.py @@ -168,7 +168,7 @@ class Dialpad(object): title = misc_utils.make_pretty(number) description = misc_utils.make_pretty(number) numbersWithDescriptions = [(number, "")] - self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) @QtCore.pyqtSlot() @QtCore.pyqtSlot(bool) @@ -183,7 +183,7 @@ class Dialpad(object): description = misc_utils.make_pretty(number) numbersWithDescriptions = [(number, "")] self._session.draft.clear() - self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) self._session.draft.call() @@ -435,7 +435,7 @@ class History(object): descriptionRows.append("%s" % "".join(rowItems)) description = "%s
" % "".join(descriptionRows) numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")] - self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) class Messages(object): @@ -676,11 +676,15 @@ class Messages(object): if not name: name = "Unknown" - contactId = str(contactDetails[QtCore.QString("id")]) + if str(contactDetails[QtCore.QString("type")]) == "Voicemail": + messageId = str(contactDetails[QtCore.QString("id")]) + else: + messageId = None + contactId = str(contactDetails[QtCore.QString("contactId")]) title = name description = unicode(contactDetails[QtCore.QString("expandedMessages")]) numbersWithDescriptions = [(number, "")] - self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions) @QtCore.pyqtSlot(QtCore.QModelIndex) @misc_utils.log_exception(_moduleLogger) @@ -918,7 +922,7 @@ class Contacts(object): ] title = name description = name - self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) @staticmethod def _choose_phonetype(numberDetails): diff --git a/src/session.py b/src/session.py index 6c2029c..f8b9bc8 100644 --- a/src/session.py +++ b/src/session.py @@ -27,7 +27,8 @@ _moduleLogger = logging.getLogger(__name__) class _DraftContact(object): - def __init__(self, title, description, numbersWithDescriptions): + def __init__(self, messageId, title, description, numbersWithDescriptions): + self.messageId = messageId self.title = title self.description = description self.numbers = numbersWithDescriptions @@ -82,11 +83,11 @@ class Draft(QtCore.QObject): message = property(_get_message, _set_message) - def add_contact(self, contactId, title, description, numbersWithDescriptions): + def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions): if self._busyReason is not None: raise RuntimeError("Please wait for %r" % self._busyReason) # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up - contactDetails = _DraftContact(title, description, numbersWithDescriptions) + contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions) self._contacts[contactId] = contactDetails self.recipientsChanged.emit() @@ -103,6 +104,9 @@ class Draft(QtCore.QObject): def get_num_contacts(self): return len(self._contacts) + def get_message_id(self, cid): + return self._contacts[cid].messageId + def get_title(self, cid): return self._contacts[cid].title @@ -204,6 +208,7 @@ class Session(QtCore.QObject): newMessages = QtCore.pyqtSignal() historyUpdated = QtCore.pyqtSignal() dndStateChange = QtCore.pyqtSignal(bool) + voicemailAvailable = QtCore.pyqtSignal(str, str) error = QtCore.pyqtSignal(str) @@ -228,6 +233,7 @@ class Session(QtCore.QObject): self._loggedInTime = self._LOGGEDOUT_TIME self._loginOps = [] self._cachePath = cachePath + self._voicemailCachePath = None self._username = None self._draft = Draft(self._pool, self._backend, self._errorLog) @@ -276,6 +282,7 @@ class Session(QtCore.QObject): self._loggedInTime = self._LOGGEDOUT_TIME self._backend[0].persist() self._save_to_cache() + self._clear_voicemail_cache() self.stateChange.emit(self.LOGGEDOUT_STATE) self.loggedOut.emit() @@ -339,6 +346,20 @@ class Session(QtCore.QObject): le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd) le.start(dnd) + def is_available(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + return os.path.exists(actualPath) + + def voicemail_path(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + if not os.path.exists(actualPath): + raise RuntimeError("Voicemail not available") + return actualPath + + def download_voicemail(self, messageId): + le = concurrent.AsyncLinearExecution(self._pool, self._download_voicemail) + le.start(messageId) + def _set_dnd(self, dnd): oldDnd = self._dnd try: @@ -436,6 +457,13 @@ class Session(QtCore.QObject): else: needOps = True + self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username) + try: + os.makedirs(self._voicemailCachePath) + except OSError, e: + if e.errno != 17: + raise + self.loggedIn.emit() self.stateChange.emit(finalState) finalState = None # Mark it as already set @@ -591,6 +619,11 @@ class Session(QtCore.QObject): self.callbackNumberChanged.emit(self._callback) self._save_to_cache() + self._clear_voicemail_cache() + + def _clear_voicemail_cache(self): + import shutil + shutil.rmtree(self._voicemailCachePath, True) def _update_account(self): try: @@ -656,6 +689,34 @@ class Session(QtCore.QObject): if oldDnd != self._dnd: self.dndStateChange(self._dnd) + def _download_voicemail(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + targetPath = "%s.%s.part" % (actualPath, time.time()) + if os.path.exists(actualPath): + self.voicemailAvailable.emit(messageId, actualPath) + return + with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"): + try: + yield ( + self._backend[0].download, + (messageId, targetPath), + {}, + ) + except Exception, e: + self.error.emit(str(e)) + return + + if os.path.exists(actualPath): + try: + os.remove(targetPath) + except: + _moduleLogger.exception("Ignoring file problems with cache") + self.voicemailAvailable.emit(messageId, actualPath) + return + else: + os.rename(targetPath, actualPath) + self.voicemailAvailable.emit(messageId, actualPath) + def _perform_op_while_loggedin(self, op): if self.state == self.LOGGEDIN_STATE: op, args, kwds = op -- 1.7.9.5