Adding voicemail support to the session and making it possible to save it
authorEd Page <eopage@byu.net>
Wed, 9 Mar 2011 00:56:32 +0000 (18:56 -0600)
committerEd Page <eopage@byu.net>
Tue, 19 Apr 2011 23:49:27 +0000 (18:49 -0500)
src/backends/gv_backend.py
src/backends/gvoice/gvoice.py
src/dialogs.py
src/gv_views.py
src/session.py

index 2414049..a4fadde 100644 (file)
@@ -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):
                """
index fda7f24..f73600e 100755 (executable)
@@ -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):
                """
index be4a177..7894103 100644 (file)
@@ -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)
index 08de9b7..18251df 100644 (file)
@@ -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("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
                        description = "<table>%s</table>" % "".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):
index 6c2029c..f8b9bc8 100644 (file)
@@ -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