← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~raoul-snyman/openlp/song-audio into lp:openlp

 

Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/song-audio into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
  Andreas Preikschat (googol)
Related bugs:
  Bug #739770 in OpenLP: "Link audio to songs"
  https://bugs.launchpad.net/openlp/+bug/739770

For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/song-audio/+merge/74002

Songs can how have audio files attached, and these files are transferred to and from services, stored in service files, and played when you display the song. There's a new pause button to pause playback.

Notable exceptions:
- openlp.org 1.x import probably doesn't support this newish setup yet.
- OpenLP 2.0 import probably doesn't either.
- I couldn't figure out how to get the volume control to display, nevermind work, so I left it out.
-- 
https://code.launchpad.net/~raoul-snyman/openlp/song-audio/+merge/74002
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/__init__.py'
--- openlp/__init__.py	2011-06-12 16:02:52 +0000
+++ openlp/__init__.py	2011-09-04 12:16:24 +0000
@@ -27,3 +27,9 @@
 """
 The :mod:`openlp` module contains all the project produced OpenLP functionality
 """
+
+import core
+import plugins
+
+__all__ = [u'core', u'plugins']
+

=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2011-09-02 11:10:07 +0000
+++ openlp/core/__init__.py	2011-09-04 12:16:24 +0000
@@ -25,7 +25,12 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
-__all__ = ('OpenLP', 'main')
+"""
+The :mod:`core` module provides all core application functions
+
+All the core functions of the OpenLP application including the GUI, settings,
+logging and a plugin framework are contained within the openlp.core module.
+"""
 
 import os
 import sys
@@ -46,16 +51,11 @@
 from openlp.core.utils import AppLocation, LanguageManager, VersionThread, \
     get_application_version, DelayStartThread
 
+
+__all__ = [u'OpenLP', u'main']
+
+
 log = logging.getLogger()
-
-
-"""
-The :mod:`core` module provides all core application functions
-
-All the core functions of the OpenLP application including the GUI, settings,
-logging and a plugin framework are contained within the openlp.core module.
-"""
-
 application_stylesheet = u"""
 QMainWindow::separator
 {

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2011-08-20 11:45:06 +0000
+++ openlp/core/lib/__init__.py	2011-09-04 12:16:24 +0000
@@ -36,6 +36,13 @@
 
 log = logging.getLogger(__name__)
 
+class MediaType(object):
+    """
+    An enumeration class for types of media.
+    """
+    Audio = 1
+    Video = 2
+
 def translate(context, text, comment=None,
     encoding=QtCore.QCoreApplication.CodecForTr, n=-1,
     translate=QtCore.QCoreApplication.translate):
@@ -241,9 +248,7 @@
 from plugin import PluginStatus, StringContent, Plugin
 from pluginmanager import PluginManager
 from settingstab import SettingsTab
-from serviceitem import ServiceItem
-from serviceitem import ServiceItemType
-from serviceitem import ItemCapabilities
+from serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
 from htmlbuilder import build_html, build_lyrics_format_css, \
     build_lyrics_outline_css
 from toolbar import OpenLPToolbar

=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	2011-08-27 18:43:05 +0000
+++ openlp/core/lib/db.py	2011-09-04 12:16:24 +0000
@@ -82,7 +82,7 @@
     load_changes = True
     try:
         tables = upgrade.upgrade_setup(metadata)
-    except SQLAlchemyError, DBAPIError:
+    except (SQLAlchemyError, DBAPIError):
         load_changes = False
     metadata_table = Table(u'metadata', metadata,
         Column(u'key', types.Unicode(64), primary_key=True),
@@ -106,7 +106,7 @@
                 getattr(upgrade, u'upgrade_%d' % version) \
                     (session, metadata, tables)
                 version_meta.value = unicode(version)
-            except SQLAlchemyError, DBAPIError:
+            except (SQLAlchemyError, DBAPIError):
                 log.exception(u'Could not run database upgrade script '
                     '"upgrade_%s", upgrade process has been halted.', version)
                 break
@@ -213,7 +213,8 @@
                 return
         try:
             self.session = init_schema(self.db_url)
-        except:
+        except (SQLAlchemyError, DBAPIError):
+            log.exception(u'Error loading database: %s', self.db_url)
             critical_error_message_box(
                 translate('OpenLP.Manager', 'Database Error'),
                 unicode(translate('OpenLP.Manager', 'OpenLP cannot load your '

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2011-08-15 22:44:02 +0000
+++ openlp/core/lib/mediamanageritem.py	2011-09-04 12:16:24 +0000
@@ -111,7 +111,7 @@
         self.requiredIcons()
         self.setupUi()
         self.retranslateUi()
-        self.auto_select_id = -1
+        self.autoSelectId = -1
         QtCore.QObject.connect(Receiver.get_receiver(),
             QtCore.SIGNAL(u'%s_service_load' % self.plugin.name),
             self.serviceLoad)
@@ -506,7 +506,7 @@
         if QtCore.QSettings().value(u'advanced/single click preview',
             QtCore.QVariant(False)).toBool() and self.quickPreviewAllowed \
             and self.listView.selectedIndexes() \
-            and self.auto_select_id == -1:
+            and self.autoSelectId == -1:
             self.onPreviewClick(True)
 
     def onPreviewClick(self, keepFocus=False):
@@ -626,7 +626,7 @@
         """
         pass
 
-    def check_search_result(self):
+    def checkSearchResult(self):
         """
         Checks if the listView is empty and adds a "No Search Results" item.
         """
@@ -662,15 +662,15 @@
             item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0]
         return item_id
 
-    def save_auto_select_id(self):
+    def saveAutoSelectId(self):
         """
         Sorts out, what item to select after loading a list.
         """
         # The item to select has not been set.
-        if self.auto_select_id == -1:
+        if self.autoSelectId == -1:
             item = self.listView.currentItem()
             if item:
-                self.auto_select_id = item.data(QtCore.Qt.UserRole).toInt()[0]
+                self.autoSelectId = item.data(QtCore.Qt.UserRole).toInt()[0]
 
     def search(self, string):
         """

=== modified file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py	2011-07-23 21:29:24 +0000
+++ openlp/core/lib/plugin.py	2011-09-04 12:16:24 +0000
@@ -368,3 +368,4 @@
         after this has been set.
         """
         self.textStrings[name] = {u'title': title, u'tooltip': tooltip}
+

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2011-08-30 15:15:39 +0000
+++ openlp/core/lib/renderer.py	2011-09-04 12:16:24 +0000
@@ -222,14 +222,14 @@
         if item.is_capable(ItemCapabilities.NoLineBreaks):
             line_end = u' '
         # Bibles
-        if item.is_capable(ItemCapabilities.AllowsWordSplit):
+        if item.is_capable(ItemCapabilities.CanWordSplit):
             pages = self._paginate_slide_words(text.split(u'\n'), line_end)
         else:
             # Clean up line endings.
             lines = self._lines_split(text)
             pages = self._paginate_slide(lines, line_end)
             # Songs and Custom
-            if item.is_capable(ItemCapabilities.AllowsVirtualSplit) and \
+            if item.is_capable(ItemCapabilities.CanSoftBreak) and \
                 len(pages) > 1 and u'[---]' in text:
                 pages = []
                 while True:

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2011-08-20 11:45:06 +0000
+++ openlp/core/lib/serviceitem.py	2011-09-04 12:16:24 +0000
@@ -52,20 +52,21 @@
     """
     Provides an enumeration of a serviceitem's capabilities
     """
-    AllowsPreview = 1
-    AllowsEdit = 2
-    AllowsMaintain = 3
+    CanPreview = 1
+    CanEdit = 2
+    CanMaintain = 3
     RequiresMedia = 4
-    AllowsLoop = 5
-    AllowsAdditions = 6
+    CanLoop = 5
+    CanAppend = 6
     NoLineBreaks = 7
     OnLoadUpdate = 8
     AddIfNewItem = 9
     ProvidesOwnDisplay = 10
-    AllowsDetailedTitleDisplay = 11
-    AllowsVariableStartTime = 12
-    AllowsVirtualSplit = 13
-    AllowsWordSplit = 14
+    HasDetailedTitleDisplay = 11
+    HasVariableStartTime = 12
+    CanSoftBreak = 13
+    CanWordSplit = 14
+    HasBackgroundAudio = 15
 
 
 class ServiceItem(object):
@@ -116,6 +117,7 @@
         self.media_length = 0
         self.from_service = False
         self.image_border = u'#000000'
+        self.background_audio = []
         self._new_item()
 
     def _new_item(self):
@@ -159,7 +161,7 @@
         """
         The render method is what generates the frames for the screen and
         obtains the display information from the renderemanager.
-        At this point all the slides are build for the given
+        At this point all the slides are built for the given
         display size.
         """
         log.debug(u'Render called')
@@ -272,7 +274,8 @@
             u'xml_version': self.xml_version,
             u'start_time': self.start_time,
             u'end_time': self.end_time,
-            u'media_length': self.media_length
+            u'media_length': self.media_length,
+            u'background_audio': self.background_audio
         }
         service_data = []
         if self.service_item_type == ServiceItemType.Text:
@@ -320,6 +323,8 @@
             self.end_time = header[u'end_time']
         if u'media_length' in header:
             self.media_length = header[u'media_length']
+        if u'background_audio' in header:
+            self.background_audio = header[u'background_audio']
         if self.service_item_type == ServiceItemType.Text:
             for slide in serviceitem[u'serviceitem'][u'data']:
                 self._raw_frames.append(slide)
@@ -341,7 +346,7 @@
         if self.is_text():
             return self.title
         else:
-            if ItemCapabilities.AllowsDetailedTitleDisplay in self.capabilities:
+            if ItemCapabilities.HasDetailedTitleDisplay in self.capabilities:
                 return self._raw_frames[0][u'title']
             elif len(self._raw_frames) > 1:
                 return self.title
@@ -359,6 +364,8 @@
         """
         self._uuid = other._uuid
         self.notes = other.notes
+        if self.is_capable(ItemCapabilities.HasBackgroundAudio):
+            log.debug(self.background_audio)
 
     def __eq__(self, other):
         """

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2011-08-22 17:32:18 +0000
+++ openlp/core/ui/maindisplay.py	2011-09-04 12:16:24 +0000
@@ -62,6 +62,10 @@
         self.override = {}
         self.retranslateUi()
         self.mediaObject = None
+        if live:
+            self.audioPlayer = AudioPlayer(self)
+        else:
+            self.audioPlayer = None
         self.firstTime = True
         self.setStyleSheet(u'border: 0px; margin: 0px; padding: 0px;')
         self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool |
@@ -587,61 +591,76 @@
         """
         log.debug(u'AudioPlayer Initialisation started')
         QtCore.QObject.__init__(self, parent)
-        self.message = None
+        self.currentIndex = -1
+        self.playlist = []
         self.mediaObject = Phonon.MediaObject()
         self.audioObject = Phonon.AudioOutput(Phonon.VideoCategory)
         Phonon.createPath(self.mediaObject, self.audioObject)
-
-    def setup(self):
-        """
-        Sets up the Audio Player for use
-        """
-        log.debug(u'AudioPlayer Setup')
-
-    def close(self):
+        QtCore.QObject.connect(self.mediaObject,
+            QtCore.SIGNAL(u'aboutToFinish()'), self.onAboutToFinish)
+
+    def __del__(self):
         """
         Shutting down so clean up connections
         """
-        self.onMediaStop()
+        self.stop()
         for path in self.mediaObject.outputPaths():
             path.disconnect()
-
-    def onMediaQueue(self, message):
-        """
-        Set up a video to play from the serviceitem.
-        """
-        log.debug(u'AudioPlayer Queue new media message %s' % message)
-        mfile = os.path.join(message[0].get_frame_path(),
-            message[0].get_frame_title())
-        self.mediaObject.setCurrentSource(Phonon.MediaSource(mfile))
-        self.onMediaPlay()
-
-    def onMediaPlay(self):
-        """
-        We want to play the play so start it
-        """
-        log.debug(u'AudioPlayer _play called')
+        QtCore.QObject.__del__(self)
+
+    def onAboutToFinish(self):
+        """
+        Just before the audio player finishes the current track, queue the next
+        item in the playlist, if there is one.
+        """
+        self.currentIndex += 1
+        if len(self.playlist) > self.currentIndex:
+            self.mediaObject.enqueue(self.playlist[self.currentIndex])
+
+    def connectVolumeSlider(self, slider):
+        slider.setAudioOutput(self.audioObject)
+
+    def reset(self):
+        """
+        Reset the audio player, clearing the playlist and the queue.
+        """
+        self.currentIndex = -1
+        self.playlist = []
+        self.stop()
+        self.mediaObject.clear()
+
+    def play(self):
+        """
+        We want to play the file so start it
+        """
+        log.debug(u'AudioPlayer.play() called')
+        if self.currentIndex == -1:
+            self.onAboutToFinish()
         self.mediaObject.play()
 
-    def onMediaPause(self):
+    def pause(self):
         """
         Pause the Audio
         """
-        log.debug(u'AudioPlayer Media paused by user')
+        log.debug(u'AudioPlayer.pause() called')
         self.mediaObject.pause()
 
-    def onMediaStop(self):
+    def stop(self):
         """
         Stop the Audio and clean up
         """
-        log.debug(u'AudioPlayer Media stopped by user')
-        self.message = None
+        log.debug(u'AudioPlayer.stop() called')
         self.mediaObject.stop()
-        self.onMediaFinish()
-
-    def onMediaFinish(self):
-        """
-        Clean up the Object queue
-        """
-        log.debug(u'AudioPlayer Reached end of media playlist')
-        self.mediaObject.clearQueue()
+
+    def addToPlaylist(self, filenames):
+        """
+        Add another file to the playlist.
+
+        ``filename``
+            The file to add to the playlist.
+        """
+        if not isinstance(filenames, list):
+            filenames = [filenames]
+        for filename in filenames:
+            self.playlist.append(Phonon.MediaSource(filename))
+

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2011-08-29 13:08:50 +0000
+++ openlp/core/ui/servicemanager.py	2011-09-04 12:16:24 +0000
@@ -28,6 +28,7 @@
 import cPickle
 import logging
 import os
+import shutil
 import zipfile
 
 log = logging.getLogger(__name__)
@@ -471,23 +472,34 @@
         if not self.fileName():
             return self.saveFileAs()
         path_file_name = unicode(self.fileName())
-        (path, file_name) = os.path.split(path_file_name)
-        (basename, extension) = os.path.splitext(file_name)
-        service_file_name = basename + '.osd'
+        path, file_name = os.path.split(path_file_name)
+        basename, extension = os.path.splitext(file_name)
+        service_file_name = '%s.osd' % basename
         log.debug(u'ServiceManager.saveFile - %s' % path_file_name)
         SettingsManager.set_last_dir(
             self.mainwindow.servicemanagerSettingsSection,
             path)
         service = []
         write_list = []
+        audio_files = []
         total_size = 0
         Receiver.send_message(u'cursor_busy')
         # Number of items + 1 to zip it
         self.mainwindow.displayProgressBar(len(self.serviceItems) + 1)
         for item in self.serviceItems:
             self.mainwindow.incrementProgressBar()
-            service.append({u'serviceitem':
-                item[u'service_item'].get_service_repr()})
+            service_item = item[u'service_item'].get_service_repr()
+            # Get all the audio files, and ready them for embedding in the
+            # service file.
+            if len(service_item[u'header'][u'background_audio']) > 0:
+                for i, filename in \
+                    enumerate(service_item[u'header'][u'background_audio']):
+                    new_file = os.path.join(u'audio', item[u'service_item']._uuid,
+                        os.path.split(filename)[1])
+                    audio_files.append((filename, new_file))
+                    service_item[u'header'][u'background_audio'][i] = new_file
+            # Add the service item to the service.
+            service.append({u'serviceitem': service_item})
             if not item[u'service_item'].uses_file():
                 continue
             skipMissing = False
@@ -541,6 +553,8 @@
             # Finally add all the listed media files.
             for path_from in write_list:
                 zip.write(path_from, path_from.encode(u'utf-8'))
+            for path_from, path_to in audio_files:
+                zip.write(path_from, path_to.encode(u'utf-8'))
         except IOError:
             log.exception(u'Failed to save service to disk')
             success = False
@@ -595,11 +609,12 @@
                         'The content encoding is not UTF-8.'))
                     continue
                 osfile = unicode(QtCore.QDir.toNativeSeparators(ucsfile))
-                filename_only = os.path.split(osfile)[1]
-                zipinfo.filename = filename_only
+                if not osfile.startswith(u'audio'):
+                    osfile = os.path.split(osfile)[1]
+                zipinfo.filename = osfile
                 zip.extract(zipinfo, self.servicePath)
-                if filename_only.endswith(u'osd'):
-                    p_file = os.path.join(self.servicePath, filename_only)
+                if osfile.endswith(u'osd'):
+                    p_file = os.path.join(self.servicePath, osfile)
             if 'p_file' in locals():
                 Receiver.send_message(u'cursor_busy')
                 fileTo = open(p_file, u'r')
@@ -630,10 +645,10 @@
                     'File is not a valid service.'))
                 log.exception(u'File contains no service data')
         except (IOError, NameError, zipfile.BadZipfile):
+            log.exception(u'Problem loading service file %s' % fileName)
             critical_error_message_box(
                 message=translate('OpenLP.ServiceManager',
                 'File could not be opened because it is corrupt.'))
-            log.exception(u'Problem loading service file %s' % fileName)
         except zipfile.BadZipfile:
             if os.path.getsize(fileName) == 0:
                 log.exception(u'Service file is zero sized: %s' % fileName)
@@ -682,16 +697,16 @@
         self.maintainAction.setVisible(False)
         self.notesAction.setVisible(False)
         self.timeAction.setVisible(False)
-        if serviceItem[u'service_item'].is_capable(ItemCapabilities.AllowsEdit)\
+        if serviceItem[u'service_item'].is_capable(ItemCapabilities.CanEdit)\
             and serviceItem[u'service_item'].edit_id:
             self.editAction.setVisible(True)
         if serviceItem[u'service_item']\
-            .is_capable(ItemCapabilities.AllowsMaintain):
+            .is_capable(ItemCapabilities.CanMaintain):
             self.maintainAction.setVisible(True)
         if item.parent() is None:
             self.notesAction.setVisible(True)
         if serviceItem[u'service_item']\
-            .is_capable(ItemCapabilities.AllowsVariableStartTime):
+            .is_capable(ItemCapabilities.HasVariableStartTime):
             self.timeAction.setVisible(True)
         self.themeMenu.menuAction().setVisible(False)
         # Set up the theme menu.
@@ -962,7 +977,7 @@
                     (unicode(translate('OpenLP.ServiceManager', 'Notes')),
                     cgi.escape(unicode(serviceitem.notes))))
             if item[u'service_item'] \
-                .is_capable(ItemCapabilities.AllowsVariableStartTime):
+                .is_capable(ItemCapabilities.HasVariableStartTime):
                 tips.append(item[u'service_item'].get_media_time())
             treewidgetitem.setToolTip(0, u'<br>'.join(tips))
             treewidgetitem.setData(0, QtCore.Qt.UserRole,
@@ -998,6 +1013,8 @@
         for file in os.listdir(self.servicePath):
             file_path = os.path.join(self.servicePath, file)
             delete_file(file_path)
+        if os.path.exists(os.path.join(self.servicePath, u'audio')):
+            shutil.rmtree(os.path.join(self.servicePath, u'audio'), False)
 
     def onThemeComboBoxSelected(self, currentIndex):
         """
@@ -1196,7 +1213,7 @@
                 item += 1
                 if self.serviceItems and item < len(self.serviceItems) and \
                     self.serviceItems[item][u'service_item'].is_capable(
-                    ItemCapabilities.AllowsPreview):
+                    ItemCapabilities.CanPreview):
                     self.mainwindow.previewController.addServiceManagerItem(
                         self.serviceItems[item][u'service_item'], 0)
                     self.mainwindow.liveController.previewListWidget.setFocus()
@@ -1214,7 +1231,7 @@
         """
         item = self.findServiceItem()[0]
         if self.serviceItems[item][u'service_item']\
-            .is_capable(ItemCapabilities.AllowsEdit):
+            .is_capable(ItemCapabilities.CanEdit):
             Receiver.send_message(u'%s_edit' %
                 self.serviceItems[item][u'service_item'].name.lower(),
                 u'L:%s' % self.serviceItems[item][u'service_item'].edit_id)
@@ -1297,7 +1314,7 @@
                     serviceItem = self.serviceItems[pos]
                     if (plugin == serviceItem[u'service_item'].name and
                         serviceItem[u'service_item'].is_capable(
-                        ItemCapabilities.AllowsAdditions)):
+                        ItemCapabilities.CanAppend)):
                         action = self.dndMenu.exec_(QtGui.QCursor.pos())
                         # New action required
                         if action == self.newAction:

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2011-07-24 19:41:33 +0000
+++ openlp/core/ui/slidecontroller.py	2011-09-04 12:16:24 +0000
@@ -256,6 +256,12 @@
             self.songMenu.setMenu(QtGui.QMenu(
                 translate('OpenLP.SlideController', 'Go To'), self.toolbar))
             self.toolbar.makeWidgetsInvisible([u'Song Menu'])
+            # Stuff for items with background audio.
+            self.audioPauseItem = self.toolbar.addToolbarButton(
+                u'Pause Audio', u':/slides/media_playback_pause.png',
+                translate('OpenLP.SlideController', 'Pause audio.'),
+                self.onAudioPauseClicked, True)
+            self.audioPauseItem.setVisible(False)
             # Build the volumeSlider.
             self.volumeSlider = QtGui.QSlider(QtCore.Qt.Horizontal)
             self.volumeSlider.setTickInterval(1)
@@ -512,13 +518,13 @@
         self.playSlidesOnce.setChecked(False)
         self.playSlidesOnce.setIcon(build_icon(u':/media/media_time.png'))
         self.playSlidesLoop.setChecked(False)
-        self.playSlidesLoop.setIcon(build_icon(u':/media/media_time.png'))       
+        self.playSlidesLoop.setIcon(build_icon(u':/media/media_time.png'))
         if item.is_text():
             if QtCore.QSettings().value(
                 self.parent().songsSettingsSection + u'/display songbar',
                 QtCore.QVariant(True)).toBool() and len(self.slideList) > 0:
                 self.toolbar.makeWidgetsVisible([u'Song Menu'])
-        if item.is_capable(ItemCapabilities.AllowsLoop) and \
+        if item.is_capable(ItemCapabilities.CanLoop) and \
             len(item.get_frames()) > 1:
             self.toolbar.makeWidgetsVisible(self.loopList)
         if item.is_media():
@@ -538,7 +544,7 @@
         self.toolbar.hide()
         self.mediabar.setVisible(False)
         self.toolbar.makeWidgetsInvisible(self.songEditList)
-        if item.is_capable(ItemCapabilities.AllowsEdit) and item.from_plugin:
+        if item.is_capable(ItemCapabilities.CanEdit) and item.from_plugin:
             self.toolbar.makeWidgetsVisible(self.songEditList)
         elif item.is_media():
             self.toolbar.setVisible(False)
@@ -576,7 +582,7 @@
         """
         Replacement item following a remote edit
         """
-        if item.__eq__(self.serviceItem):
+        if item == self.serviceItem:
             self._processItem(item, self.previewListWidget.currentRow())
 
     def addServiceManagerItem(self, item, slideno):
@@ -586,15 +592,17 @@
         Called by ServiceManager
         """
         log.debug(u'addServiceManagerItem live = %s' % self.isLive)
-        # If no valid slide number is specified we take the first one.
+        # If no valid slide number is specified we take the first one, but we
+        # remember the initial value to see if we should reload the song or not
+        slidenum = slideno
         if slideno == -1:
-            slideno = 0
-        # If service item is the same as the current on only change slide
-        if item.__eq__(self.serviceItem):
-            self.__checkUpdateSelectedSlide(slideno)
+            slidenum = 0
+        # If service item is the same as the current one, only change slide
+        if slideno >= 0 and item == self.serviceItem:
+            self.__checkUpdateSelectedSlide(slidenum)
             self.slideSelected()
-            return
-        self._processItem(item, slideno)
+        else:
+            self._processItem(item, slidenum)
 
     def _processItem(self, serviceItem, slideno):
         """
@@ -618,6 +626,15 @@
         self.previewListWidget.setColumnWidth(0, width)
         if self.isLive:
             self.songMenu.menu().clear()
+            self.display.audioPlayer.reset()
+            self.setAudioItemsVisibility(False)
+            self.audioPauseItem.setChecked(False)
+            if self.serviceItem.is_capable(ItemCapabilities.HasBackgroundAudio):
+                log.debug(u'Starting to play...')
+                self.display.audioPlayer.addToPlaylist(
+                    self.serviceItem.background_audio)
+                self.display.audioPlayer.play()
+                self.setAudioItemsVisibility(True)
         row = 0
         text = []
         for framenumber, frame in enumerate(self.serviceItem.get_frames()):
@@ -1097,6 +1114,17 @@
         self.playSlidesLoop.setChecked(False)
         self.onToggleLoop()
 
+    def setAudioItemsVisibility(self, visible):
+        self.audioPauseItem.setVisible(visible)
+
+    def onAudioPauseClicked(self, checked):
+        if not self.audioPauseItem.isVisible():
+            return
+        if checked:
+            self.display.audioPlayer.pause()
+        else:
+            self.display.audioPlayer.play()
+
     def timerEvent(self, event):
         """
         If the timer event is for this window select next slide

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py'
--- openlp/plugins/bibles/lib/mediaitem.py	2011-08-29 12:47:32 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -67,7 +67,7 @@
         self.hasSearch = True
         self.search_results = {}
         self.second_search_results = {}
-        self.check_search_result()
+        self.checkSearchResult()
         QtCore.QObject.connect(Receiver.get_receiver(),
             QtCore.SIGNAL(u'bibles_load_list'), self.reloadBibles)
 
@@ -651,7 +651,7 @@
         elif self.search_results:
             self.displayResults(bible, second_bible)
         self.advancedSearchButton.setEnabled(True)
-        self.check_search_result()
+        self.checkSearchResult()
         Receiver.send_message(u'cursor_normal')
         Receiver.send_message(u'openlp_process_events')
 
@@ -715,7 +715,7 @@
         elif self.search_results:
             self.displayResults(bible, second_bible)
         self.quickSearchButton.setEnabled(True)
-        self.check_search_result()
+        self.checkSearchResult()
         Receiver.send_message(u'cursor_normal')
         Receiver.send_message(u'openlp_process_events')
 
@@ -863,9 +863,9 @@
             not second_bible:
             # Split the line but do not replace line breaks in renderer.
             service_item.add_capability(ItemCapabilities.NoLineBreaks)
-        service_item.add_capability(ItemCapabilities.AllowsPreview)
-        service_item.add_capability(ItemCapabilities.AllowsLoop)
-        service_item.add_capability(ItemCapabilities.AllowsWordSplit)
+        service_item.add_capability(ItemCapabilities.CanPreview)
+        service_item.add_capability(ItemCapabilities.CanLoop)
+        service_item.add_capability(ItemCapabilities.CanWordSplit)
         # Service Item: Title
         service_item.title = u', '.join(raw_title)
         # Service Item: Theme

=== modified file 'openlp/plugins/custom/forms/editcustomform.py'
--- openlp/plugins/custom/forms/editcustomform.py	2011-06-29 08:04:55 +0000
+++ openlp/plugins/custom/forms/editcustomform.py	2011-09-04 12:16:24 +0000
@@ -135,7 +135,7 @@
         self.customSlide.credits = unicode(self.creditEdit.text())
         self.customSlide.theme_name = unicode(self.themeComboBox.currentText())
         success = self.manager.save_object(self.customSlide)
-        self.mediaitem.auto_select_id = self.customSlide.id
+        self.mediaitem.autoSelectId = self.customSlide.id
         return success
 
     def onUpButtonClicked(self):

=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
--- openlp/plugins/custom/lib/mediaitem.py	2011-08-25 19:52:07 +0000
+++ openlp/plugins/custom/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -132,7 +132,7 @@
 
     def loadList(self, custom_slides):
         # Sort out what custom we want to select after loading the list.
-        self.save_auto_select_id()
+        self.saveAutoSelectId()
         self.listView.clear()
         # Sort the customs by its title considering language specific
         # characters. lower() is needed for windows!
@@ -144,9 +144,9 @@
                 QtCore.Qt.UserRole, QtCore.QVariant(custom_slide.id))
             self.listView.addItem(custom_name)
             # Auto-select the custom.
-            if custom_slide.id == self.auto_select_id:
+            if custom_slide.id == self.autoSelectId:
                 self.listView.setCurrentItem(custom_name)
-        self.auto_select_id = -1
+        self.autoSelectId = -1
         # Called to redisplay the custom list screen edith from a search
         # or from the exit of the Custom edit dialog. If remote editing is
         # active trigger it and clean up so it will not update again.
@@ -180,7 +180,7 @@
             self.remoteTriggered = remote_type
             self.edit_custom_form.loadCustom(custom_id, (remote_type == u'P'))
             self.edit_custom_form.exec_()
-            self.auto_select_id = -1
+            self.autoSelectId = -1
             self.onSearchTextButtonClick()
 
     def onEditClick(self):
@@ -192,7 +192,7 @@
             item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0]
             self.edit_custom_form.loadCustom(item_id, False)
             self.edit_custom_form.exec_()
-            self.auto_select_id = -1
+            self.autoSelectId = -1
             self.onSearchTextButtonClick()
 
     def onDeleteClick(self):
@@ -227,10 +227,10 @@
         slide = None
         theme = None
         item_id = self._getIdOfItemToGenerate(item, self.remoteCustom)
-        service_item.add_capability(ItemCapabilities.AllowsEdit)
-        service_item.add_capability(ItemCapabilities.AllowsPreview)
-        service_item.add_capability(ItemCapabilities.AllowsLoop)
-        service_item.add_capability(ItemCapabilities.AllowsVirtualSplit)
+        service_item.add_capability(ItemCapabilities.CanEdit)
+        service_item.add_capability(ItemCapabilities.CanPreview)
+        service_item.add_capability(ItemCapabilities.CanLoop)
+        service_item.add_capability(ItemCapabilities.CanSoftBreak)
         customSlide = self.plugin.manager.get_object(CustomSlide, item_id)
         title = customSlide.title
         credit = customSlide.credits
@@ -273,7 +273,7 @@
                 CustomSlide.theme_name.like(u'%' + self.whitespace.sub(u' ',
                 search_keywords) + u'%'), order_by_ref=CustomSlide.title)
             self.loadList(search_results)
-        self.check_search_result()
+        self.checkSearchResult()
 
     def onSearchTextEditChanged(self, text):
         """

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2011-08-22 17:32:18 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -149,10 +149,10 @@
             if not items:
                 return False
         service_item.title = unicode(self.plugin.nameStrings[u'plural'])
-        service_item.add_capability(ItemCapabilities.AllowsMaintain)
-        service_item.add_capability(ItemCapabilities.AllowsPreview)
-        service_item.add_capability(ItemCapabilities.AllowsLoop)
-        service_item.add_capability(ItemCapabilities.AllowsAdditions)
+        service_item.add_capability(ItemCapabilities.CanMaintain)
+        service_item.add_capability(ItemCapabilities.CanPreview)
+        service_item.add_capability(ItemCapabilities.CanLoop)
+        service_item.add_capability(ItemCapabilities.CanAppend)
         # force a nonexistent theme
         service_item.theme = -1
         missing_items = []

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2011-08-02 05:07:09 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -31,11 +31,11 @@
 import locale
 
 from PyQt4 import QtCore, QtGui
+from PyQt4.phonon import Phonon
 
 from openlp.core.lib import MediaManagerItem, build_icon, ItemCapabilities, \
-    SettingsManager, translate, check_item_selected, Receiver
+    SettingsManager, translate, check_item_selected, Receiver, MediaType
 from openlp.core.lib.ui import UiStrings, critical_error_message_box
-from PyQt4.phonon import Phonon
 
 log = logging.getLogger(__name__)
 
@@ -48,9 +48,9 @@
     log.info(u'%s MediaMediaItem loaded', __name__)
 
     def __init__(self, parent, plugin, icon):
-        self.IconPath = u'images/image'
+        self.iconPath = u'images/image'
         self.background = False
-        self.PreviewFunction = CLAPPERBOARD
+        self.previewFunction = CLAPPERBOARD
         MediaManagerItem.__init__(self, parent, plugin, icon)
         self.singleServiceItem = False
         self.hasSearch = True
@@ -139,8 +139,8 @@
             # File is no longer present
             critical_error_message_box(
                 translate('MediaPlugin.MediaItem', 'Missing Media File'),
-                unicode(translate('MediaPlugin.MediaItem',
-                'The file %s no longer exists.')) % filename)
+                    unicode(translate('MediaPlugin.MediaItem',
+                        'The file %s no longer exists.')) % filename)
             return False
         self.mediaObject.stop()
         self.mediaObject.clearQueue()
@@ -156,13 +156,16 @@
                 or self.mediaObject.currentSource().type() \
                 == Phonon.MediaSource.Invalid:
                 self.mediaObject.stop()
-                critical_error_message_box(UiStrings().UnsupportedFile,
-                        UiStrings().UnsupportedFile)
+                critical_error_message_box(
+                    translate('MediaPlugin.MediaItem', 'File Too Big'),
+                    translate('MediaPlugin.MediaItem', 'The file you are '
+                        'trying to load is too big. Please reduce it to less '
+                        'than 50MiB.'))
                 return False
             self.mediaObject.stop()
             service_item.media_length = self.mediaObject.totalTime() / 1000
             service_item.add_capability(
-                ItemCapabilities.AllowsVariableStartTime)
+                ItemCapabilities.HasVariableStartTime)
         service_item.title = unicode(self.plugin.nameStrings[u'singular'])
         service_item.add_capability(ItemCapabilities.RequiresMedia)
         # force a non-existent theme
@@ -217,6 +220,19 @@
             item_name.setToolTip(track)
             self.listView.addItem(item_name)
 
+    def getList(self, type=MediaType.Audio):
+        media = SettingsManager.load_list(self.settingsSection, u'media')
+        media.sort(cmp=locale.strcoll,
+            key=lambda filename: os.path.split(unicode(filename))[1].lower())
+        ext = []
+        if type == MediaType.Audio:
+            ext = self.plugin.audio_extensions_list
+        else:
+            ext = self.plugin.video_extensions_list
+        ext = map(lambda x: x[1:], ext)
+        media = filter(lambda x: os.path.splitext(x)[1] in ext, media)
+        return media
+
     def createPhonon(self):
         log.debug(u'CreatePhonon')
         if not self.mediaObject:

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2011-08-02 05:07:09 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -248,7 +248,7 @@
         service_item.title = unicode(self.displayTypeComboBox.currentText())
         service_item.shortname = unicode(self.displayTypeComboBox.currentText())
         service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
-        service_item.add_capability(ItemCapabilities.AllowsDetailedTitleDisplay)
+        service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay)
         shortname = service_item.shortname
         if shortname:
             for bitem in items:

=== modified file 'openlp/plugins/songs/forms/__init__.py'
--- openlp/plugins/songs/forms/__init__.py	2011-06-12 16:02:52 +0000
+++ openlp/plugins/songs/forms/__init__.py	2011-09-04 12:16:24 +0000
@@ -52,6 +52,7 @@
 from the .ui files later if necessary.
 """
 
+from mediafilesform import MediaFilesForm
 from authorsform import AuthorsForm
 from topicsform import TopicsForm
 from songbookform import SongBookForm

=== modified file 'openlp/plugins/songs/forms/editsongdialog.py'
--- openlp/plugins/songs/forms/editsongdialog.py	2011-06-14 19:55:17 +0000
+++ openlp/plugins/songs/forms/editsongdialog.py	2011-09-04 12:16:24 +0000
@@ -28,7 +28,8 @@
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import build_icon, translate
-from openlp.core.lib.ui import UiStrings, create_accept_reject_button_box
+from openlp.core.lib.ui import UiStrings, create_accept_reject_button_box, \
+    create_up_down_push_button_set
 from openlp.plugins.songs.lib.ui import SongStrings
 
 class Ui_EditSongDialog(object):
@@ -36,9 +37,11 @@
         editSongDialog.setObjectName(u'editSongDialog')
         editSongDialog.resize(650, 400)
         editSongDialog.setWindowIcon(
-            build_icon(u':/icon/openlp.org-icon-32.bmp'))
+            build_icon(u':/icon/openlp-logo-16x16.png'))
         editSongDialog.setModal(True)
         self.dialogLayout = QtGui.QVBoxLayout(editSongDialog)
+        self.dialogLayout.setSpacing(8)
+        self.dialogLayout.setContentsMargins(8, 8, 8, 8)
         self.dialogLayout.setObjectName(u'dialogLayout')
         self.songTabWidget = QtGui.QTabWidget(editSongDialog)
         self.songTabWidget.setObjectName(u'songTabWidget')
@@ -246,6 +249,36 @@
         self.commentsLayout.addWidget(self.commentsEdit)
         self.themeTabLayout.addWidget(self.commentsGroupBox)
         self.songTabWidget.addTab(self.themeTab, u'')
+        # audio tab
+        self.audioTab = QtGui.QWidget()
+        self.audioTab.setObjectName(u'audioTab')
+        self.audioLayout = QtGui.QHBoxLayout(self.audioTab)
+        self.audioLayout.setObjectName(u'audioLayout')
+        self.audioListWidget = QtGui.QListWidget(self.audioTab)
+        self.audioListWidget.setObjectName(u'audioListWidget')
+        self.audioLayout.addWidget(self.audioListWidget)
+        self.audioButtonsLayout = QtGui.QVBoxLayout()
+        self.audioButtonsLayout.setObjectName(u'audioButtonsLayout')
+        self.audioAddFromFileButton = QtGui.QPushButton(self.audioTab)
+        self.audioAddFromFileButton.setObjectName(u'audioAddFromFileButton')
+        self.audioButtonsLayout.addWidget(self.audioAddFromFileButton)
+        self.audioAddFromMediaButton = QtGui.QPushButton(self.audioTab)
+        self.audioAddFromMediaButton.setObjectName(u'audioAddFromMediaButton')
+        self.audioButtonsLayout.addWidget(self.audioAddFromMediaButton)
+        self.audioRemoveButton = QtGui.QPushButton(self.audioTab)
+        self.audioRemoveButton.setObjectName(u'audioRemoveButton')
+        self.audioButtonsLayout.addWidget(self.audioRemoveButton)
+        self.audioRemoveAllButton = QtGui.QPushButton(self.audioTab)
+        self.audioRemoveAllButton.setObjectName(u'audioRemoveAllButton')
+        self.audioButtonsLayout.addWidget(self.audioRemoveAllButton)
+        self.audioButtonsLayout.addStretch(1)
+        self.upButton, self.downButton = \
+            create_up_down_push_button_set(self)
+        self.audioButtonsLayout.addWidget(self.upButton)
+        self.audioButtonsLayout.addWidget(self.downButton)
+        self.audioLayout.addLayout(self.audioButtonsLayout)
+        self.songTabWidget.addTab(self.audioTab, u'')
+        # Last few bits
         self.dialogLayout.addWidget(self.songTabWidget)
         self.buttonBox = create_accept_reject_button_box(editSongDialog)
         self.dialogLayout.addWidget(self.buttonBox)
@@ -305,6 +338,17 @@
             self.songTabWidget.indexOf(self.themeTab),
             translate('SongsPlugin.EditSongForm',
                 'Theme, Copyright Info && Comments'))
+        self.songTabWidget.setTabText(
+            self.songTabWidget.indexOf(self.audioTab),
+            translate('SongsPlugin.EditSongForm', 'Linked Audio'))
+        self.audioAddFromFileButton.setText(
+            translate('SongsPlugin.EditSongForm', 'Add &File(s)'))
+        self.audioAddFromMediaButton.setText(
+            translate('SongsPlugin.EditSongForm', 'Add &Media'))
+        self.audioRemoveButton.setText(
+            translate('SongsPlugin.EditSongForm', '&Remove'))
+        self.audioRemoveAllButton.setText(
+            translate('SongsPlugin.EditSongForm', 'Remove &All'))
 
 def editSongDialogComboBox(parent, name):
     """

=== modified file 'openlp/plugins/songs/forms/editsongform.py'
--- openlp/plugins/songs/forms/editsongform.py	2011-07-06 14:00:32 +0000
+++ openlp/plugins/songs/forms/editsongform.py	2011-09-04 12:16:24 +0000
@@ -27,15 +27,18 @@
 
 import logging
 import re
+import os
+import shutil
 
 from PyQt4 import QtCore, QtGui
 
-from openlp.core.lib import Receiver, translate
+from openlp.core.lib import PluginStatus, Receiver, MediaType, translate
 from openlp.core.lib.ui import UiStrings, add_widget_completer, \
     critical_error_message_box, find_and_set_in_combo_box
-from openlp.plugins.songs.forms import EditVerseForm
+from openlp.core.utils import AppLocation
+from openlp.plugins.songs.forms import EditVerseForm, MediaFilesForm
 from openlp.plugins.songs.lib import SongXML, VerseType, clean_song
-from openlp.plugins.songs.lib.db import Book, Song, Author, Topic
+from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile
 from openlp.plugins.songs.lib.ui import SongStrings
 from editsongdialog import Ui_EditSongDialog
 
@@ -93,6 +96,14 @@
             self.mediaitem.plugin.renderer.themeManager.onAddTheme)
         QtCore.QObject.connect(self.maintenanceButton,
             QtCore.SIGNAL(u'clicked()'), self.onMaintenanceButtonClicked)
+        QtCore.QObject.connect(self.audioAddFromFileButton,
+            QtCore.SIGNAL(u'clicked()'), self.onAudioAddFromFileButtonClicked)
+        QtCore.QObject.connect(self.audioAddFromMediaButton,
+            QtCore.SIGNAL(u'clicked()'), self.onAudioAddFromMediaButtonClicked)
+        QtCore.QObject.connect(self.audioRemoveButton,
+            QtCore.SIGNAL(u'clicked()'), self.onAudioRemoveButtonClicked)
+        QtCore.QObject.connect(self.audioRemoveAllButton,
+            QtCore.SIGNAL(u'clicked()'), self.onAudioRemoveAllButtonClicked)
         QtCore.QObject.connect(Receiver.get_receiver(),
             QtCore.SIGNAL(u'theme_update_list'), self.loadThemes)
         self.previewButton = QtGui.QPushButton()
@@ -104,12 +115,14 @@
             QtCore.SIGNAL(u'clicked(QAbstractButton*)'), self.onPreview)
         # Create other objects and forms
         self.manager = manager
-        self.verse_form = EditVerseForm(self)
+        self.verseForm = EditVerseForm(self)
+        self.mediaForm = MediaFilesForm(self)
         self.initialise()
         self.authorsListView.setSortingEnabled(False)
         self.authorsListView.setAlternatingRowColors(True)
         self.topicsListView.setSortingEnabled(False)
         self.topicsListView.setAlternatingRowColors(True)
+        self.audioListWidget.setAlternatingRowColors(True)
         self.findVerseSplit = re.compile(u'---\[\]---\n', re.UNICODE)
         self.whitespace = re.compile(r'\W+', re.UNICODE)
 
@@ -161,6 +174,16 @@
             self.themes.append(theme)
         add_widget_completer(self.themes, self.themeComboBox)
 
+    def loadMediaFiles(self):
+        self.audioAddFromMediaButton.setVisible(False)
+        for plugin in self.parent().pluginManager.plugins:
+            if plugin.name == u'media' and \
+                plugin.status == PluginStatus.Active:
+                self.audioAddFromMediaButton.setVisible(True)
+                self.mediaForm.populateFiles(
+                    plugin.getMediaManagerItem().getList(MediaType.Audio))
+                break
+
     def newSong(self):
         log.debug(u'New Song')
         self.song = None
@@ -176,11 +199,13 @@
         self.verseListWidget.setRowCount(0)
         self.authorsListView.clear()
         self.topicsListView.clear()
+        self.audioListWidget.clear()
         self.titleEdit.setFocus(QtCore.Qt.OtherFocusReason)
         self.songBookNumberEdit.setText(u'')
         self.loadAuthors()
         self.loadTopics()
         self.loadBooks()
+        self.loadMediaFiles()
         self.themeComboBox.setCurrentIndex(0)
         # it's a new song to preview is not possible
         self.previewButton.setVisible(False)
@@ -201,6 +226,7 @@
         self.loadAuthors()
         self.loadTopics()
         self.loadBooks()
+        self.loadMediaFiles()
         self.song = self.manager.get_object(Song, id)
         self.titleEdit.setText(self.song.title)
         if self.song.alternate_title:
@@ -303,6 +329,11 @@
             topic_name = QtGui.QListWidgetItem(unicode(topic.name))
             topic_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(topic.id))
             self.topicsListView.addItem(topic_name)
+        self.audioListWidget.clear()
+        for media in self.song.media_files:
+            media_file = QtGui.QListWidgetItem(os.path.split(media.file_name)[1])
+            media_file.setData(QtCore.Qt.UserRole, QtCore.QVariant(media.file_name))
+            self.audioListWidget.addItem(media_file)
         self.titleEdit.setFocus(QtCore.Qt.OtherFocusReason)
         # Hide or show the preview button.
         self.previewButton.setVisible(preview)
@@ -436,9 +467,9 @@
         self.verseDeleteButton.setEnabled(True)
 
     def onVerseAddButtonClicked(self):
-        self.verse_form.setVerse(u'', True)
-        if self.verse_form.exec_():
-            after_text, verse_tag, verse_num = self.verse_form.getVerse()
+        self.verseForm.setVerse(u'', True)
+        if self.verseForm.exec_():
+            after_text, verse_tag, verse_num = self.verseForm.getVerse()
             verse_def = u'%s%s' % (verse_tag, verse_num)
             item = QtGui.QTableWidgetItem(after_text)
             item.setData(QtCore.Qt.UserRole, QtCore.QVariant(verse_def))
@@ -454,20 +485,21 @@
         if item:
             tempText = item.text()
             verseId = unicode(item.data(QtCore.Qt.UserRole).toString())
-            self.verse_form.setVerse(tempText, True, verseId)
-            if self.verse_form.exec_():
-                after_text, verse_tag, verse_num = self.verse_form.getVerse()
+            self.verseForm.setVerse(tempText, True, verseId)
+            if self.verseForm.exec_():
+                after_text, verse_tag, verse_num = self.verseForm.getVerse()
                 verse_def = u'%s%s' % (verse_tag, verse_num)
                 item.setData(QtCore.Qt.UserRole, QtCore.QVariant(verse_def))
                 item.setText(after_text)
-                # number of lines has change so repaint the list moving the data
+                # number of lines has changed, repaint the list moving the data
                 if len(tempText.split(u'\n')) != len(after_text.split(u'\n')):
                     tempList = {}
                     tempId = {}
                     for row in range(0, self.verseListWidget.rowCount()):
-                        tempList[row] = self.verseListWidget.item(row, 0).text()
-                        tempId[row] = self.verseListWidget.item(row, 0).\
-                            data(QtCore.Qt.UserRole)
+                        tempList[row] = self.verseListWidget.item(row, 0)\
+                            .text()
+                        tempId[row] = self.verseListWidget.item(row, 0)\
+                            .data(QtCore.Qt.UserRole)
                     self.verseListWidget.clear()
                     for row in range (0, len(tempList)):
                         item = QtGui.QTableWidgetItem(tempList[row], 0)
@@ -486,12 +518,12 @@
                 verse_list += u'---[%s:%s]---\n' % (verse_tag, verse_num)
                 verse_list += item.text()
                 verse_list += u'\n'
-            self.verse_form.setVerse(verse_list)
+            self.verseForm.setVerse(verse_list)
         else:
-            self.verse_form.setVerse(u'')
-        if not self.verse_form.exec_():
+            self.verseForm.setVerse(u'')
+        if not self.verseForm.exec_():
             return
-        verse_list = self.verse_form.getVerseAll()
+        verse_list = self.verseForm.getVerseAll()
         verse_list = unicode(verse_list.replace(u'\r\n', u'\n'))
         self.verseListWidget.clear()
         self.verseListWidget.setRowCount(0)
@@ -670,6 +702,66 @@
             self.saveSong(True)
             Receiver.send_message(u'songs_preview')
 
+    def onAudioAddFromFileButtonClicked(self):
+        """
+        Loads file(s) from the filesystem.
+        """
+        filters = u'%s (*)' % UiStrings().AllFiles
+        filenames = QtGui.QFileDialog.getOpenFileNames(self,
+            translate('SongsPlugin.EditSongForm', 'Open File(s)'),
+            QtCore.QString(), filters)
+        for filename in filenames:
+            item = QtGui.QListWidgetItem(os.path.split(unicode(filename))[1])
+            item.setData(QtCore.Qt.UserRole, filename)
+            self.audioListWidget.addItem(item)
+
+    def onAudioAddFromMediaButtonClicked(self):
+        """
+        Loads file(s) from the media plugin.
+        """
+        if self.mediaForm.exec_():
+            for filename in self.mediaForm.getSelectedFiles():
+                item = QtGui.QListWidgetItem(os.path.split(unicode(filename))[1])
+                item.setData(QtCore.Qt.UserRole, filename)
+                self.audioListWidget.addItem(item)
+
+    def onAudioRemoveButtonClicked(self):
+        """
+        Removes a file from the list.
+        """
+        row = self.audioListWidget.currentRow()
+        if row == -1:
+            return
+        self.audioListWidget.takeItem(row)
+
+    def onAudioRemoveAllButtonClicked(self):
+        """
+        Removes all files from the list.
+        """
+        self.audioListWidget.clear()
+
+    def onUpButtonClicked(self):
+        """
+        Moves a file up when the user clicks the up button on the audio tab.
+        """
+        row = self.audioListWidget.currentRow()
+        if row <= 0:
+            return
+        item = self.audioListWidget.takeItem(row)
+        self.audioListWidget.insertItem(row - 1, item)
+        self.audioListWidget.setCurrentRow(row - 1)
+
+    def onDownButtonClicked(self):
+        """
+        Moves a file down when the user clicks the up button on the audio tab.
+        """
+        row = self.audioListWidget.currentRow()
+        if row == -1 or row > self.audioListWidget.count() - 1:
+            return
+        item = self.audioListWidget.takeItem(row)
+        self.audioListWidget.insertItem(row + 1, item)
+        self.audioListWidget.setCurrentRow(row + 1)
+
     def clearCaches(self):
         """
         Free up autocompletion memory on dialog exit
@@ -744,18 +836,55 @@
             self.song.theme_name = None
         self._processLyrics()
         self.song.authors = []
-        for row in range(self.authorsListView.count()):
+        for row in xrange(self.authorsListView.count()):
             item = self.authorsListView.item(row)
             authorId = (item.data(QtCore.Qt.UserRole)).toInt()[0]
             self.song.authors.append(self.manager.get_object(Author, authorId))
         self.song.topics = []
-        for row in range(self.topicsListView.count()):
+        for row in xrange(self.topicsListView.count()):
             item = self.topicsListView.item(row)
             topicId = (item.data(QtCore.Qt.UserRole)).toInt()[0]
             self.song.topics.append(self.manager.get_object(Topic, topicId))
-        clean_song(self.manager, self.song)
-        self.manager.save_object(self.song)
-        self.mediaitem.auto_select_id = self.song.id
+        # Save the song here because we need a valid id for the audio files.
+        clean_song(self.manager, self.song)
+        self.manager.save_object(self.song)
+        audio_files = map(lambda a: a.file_name, self.song.media_files)
+        log.debug(audio_files)
+        save_path = os.path.join(
+            AppLocation.get_section_data_path(self.mediaitem.plugin.name),
+            'audio', str(self.song.id))
+        if not os.path.exists(save_path):
+            os.makedirs(save_path)
+        self.song.media_files = []
+        files = []
+        for row in xrange(self.audioListWidget.count()):
+            item = self.audioListWidget.item(row)
+            filename = unicode(item.data(QtCore.Qt.UserRole).toString())
+            if not filename.startswith(save_path):
+                oldfile, filename = filename, os.path.join(save_path,
+                    os.path.split(filename)[1])
+                shutil.copyfile(oldfile, filename)
+            files.append(filename)
+            media_file = MediaFile()
+            media_file.file_name = filename
+            media_file.type = u'audio'
+            media_file.weight = row
+            self.song.media_files.append(media_file)
+        for audio in audio_files:
+            if audio not in files:
+                try:
+                    os.remove(audio)
+                except:
+                    log.exception('Could not remove file: %s', audio)
+                    pass
+        if not files:
+            try:
+                os.rmdir(save_path)
+            except OSError:
+                log.exception(u'Could not remove directory: %s', save_path)
+        clean_song(self.manager, self.song)
+        self.manager.save_object(self.song)
+        self.mediaitem.autoSelectId = self.song.id
 
     def _processLyrics(self):
         """
@@ -783,3 +912,4 @@
         except:
             log.exception(u'Problem processing song Lyrics \n%s',
                 sxml.dump_xml())
+

=== added file 'openlp/plugins/songs/forms/mediafilesdialog.py'
--- openlp/plugins/songs/forms/mediafilesdialog.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/forms/mediafilesdialog.py	2011-09-04 12:16:24 +0000
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan,      #
+# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias     #
+# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,    #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund             #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+from PyQt4 import QtCore, QtGui
+
+from openlp.core.lib import translate, build_icon
+
+class Ui_MediaFilesDialog(object):
+    def setupUi(self, mediaFilesDialog):
+        mediaFilesDialog.setObjectName(u'mediaFilesDialog')
+        mediaFilesDialog.setWindowModality(QtCore.Qt.ApplicationModal)
+        mediaFilesDialog.resize(400, 300)
+        mediaFilesDialog.setModal(True)
+        mediaFilesDialog.setWindowIcon(
+            build_icon(u':/icon/openlp-logo-16x16.png'))
+        self.filesVerticalLayout = QtGui.QVBoxLayout(mediaFilesDialog)
+        self.filesVerticalLayout.setSpacing(8)
+        self.filesVerticalLayout.setMargin(8)
+        self.filesVerticalLayout.setObjectName(u'filesVerticalLayout')
+        self.selectLabel = QtGui.QLabel(mediaFilesDialog)
+        self.selectLabel.setWordWrap(True)
+        self.selectLabel.setObjectName(u'selectLabel')
+        self.filesVerticalLayout.addWidget(self.selectLabel)
+        self.fileListWidget = QtGui.QListWidget(mediaFilesDialog)
+        self.fileListWidget.setAlternatingRowColors(True)
+        self.fileListWidget.setSelectionMode(
+            QtGui.QAbstractItemView.ExtendedSelection)
+        self.fileListWidget.setObjectName(u'fileListWidget')
+        self.filesVerticalLayout.addWidget(self.fileListWidget)
+        self.buttonBox = QtGui.QDialogButtonBox(mediaFilesDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+        self.buttonBox.setStandardButtons(
+            QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok)
+        self.buttonBox.setObjectName(u'buttonBox')
+        self.filesVerticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(mediaFilesDialog)
+        QtCore.QObject.connect(self.buttonBox,
+            QtCore.SIGNAL(u'accepted()'), mediaFilesDialog.accept)
+        QtCore.QObject.connect(self.buttonBox,
+            QtCore.SIGNAL(u'rejected()'), mediaFilesDialog.reject)
+        QtCore.QMetaObject.connectSlotsByName(mediaFilesDialog)
+
+    def retranslateUi(self, mediaFilesDialog):
+        mediaFilesDialog.setWindowTitle(
+            translate('SongsPlugin.MediaFilesForm', 'Select Media File(s)'))
+        self.selectLabel.setText(
+            translate('SongsPlugin.MediaFilesForm', u'Select one or more '
+                'audio files from the list below, and click OK to import them '
+                'into this song.'))
+

=== added file 'openlp/plugins/songs/forms/mediafilesform.py'
--- openlp/plugins/songs/forms/mediafilesform.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/forms/mediafilesform.py	2011-09-04 12:16:24 +0000
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan,      #
+# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias     #
+# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,    #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund             #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import logging
+import os
+
+from PyQt4 import QtCore, QtGui
+
+from mediafilesdialog import Ui_MediaFilesDialog
+
+log = logging.getLogger(__name__)
+
+class MediaFilesForm(QtGui.QDialog, Ui_MediaFilesDialog):
+    """
+    Class to show a list of files from the
+    """
+    log.info(u'%s MediaFilesForm loaded', __name__)
+
+    def __init__(self, parent):
+        QtGui.QDialog.__init__(self)
+        self.setupUi(self)
+
+    def populateFiles(self, files):
+        self.fileListWidget.clear()
+        for file in files:
+            item = QtGui.QListWidgetItem(os.path.split(file)[1])
+            item.setData(QtCore.Qt.UserRole, file)
+            self.fileListWidget.addItem(item)
+
+    def getSelectedFiles(self):
+        return map(lambda x: unicode(x.data(QtCore.Qt.UserRole).toString()),
+            self.fileListWidget.selectedItems())
+

=== modified file 'openlp/plugins/songs/lib/db.py'
--- openlp/plugins/songs/lib/db.py	2011-08-26 13:13:32 +0000
+++ openlp/plugins/songs/lib/db.py	2011-09-04 12:16:24 +0000
@@ -232,7 +232,8 @@
             'authors': relation(Author, backref='songs',
                 secondary=authors_songs_table, lazy=False),
             'book': relation(Book, backref='songs'),
-            'media_files': relation(MediaFile, backref='songs'),
+            'media_files': relation(MediaFile, backref='songs',
+                order_by=media_files_table.c.weight),
             'topics': relation(Topic, backref='songs',
                 secondary=songs_topics_table)
         })

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2011-07-07 17:03:38 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2011-09-04 12:16:24 +0000
@@ -28,6 +28,8 @@
 import logging
 import locale
 import re
+import os
+import shutil
 
 from PyQt4 import QtCore, QtGui
 from sqlalchemy.sql import or_
@@ -37,11 +39,12 @@
 from openlp.core.lib.searchedit import SearchEdit
 from openlp.core.lib.ui import UiStrings, context_menu_action, \
     context_menu_separator
+from openlp.core.utils import AppLocation
 from openlp.plugins.songs.forms import EditSongForm, SongMaintenanceForm, \
     SongImportForm, SongExportForm
 from openlp.plugins.songs.lib import OpenLyrics, SongXML, VerseType, \
     clean_string
-from openlp.plugins.songs.lib.db import Author, Song
+from openlp.plugins.songs.lib.db import Author, Song, MediaFile
 from openlp.plugins.songs.lib.ui import SongStrings
 
 log = logging.getLogger(__name__)
@@ -79,6 +82,22 @@
         self.quickPreviewAllowed = True
         self.hasSearch = True
 
+    def _updateBackgroundAudio(self, song, item):
+        song.media_files = []
+        for i, bga in enumerate(item.background_audio):
+            dest_file = os.path.join(
+                AppLocation.get_section_data_path(self.plugin.name),
+                u'audio', str(song.id), os.path.split(bga)[1])
+            if not os.path.exists(os.path.split(dest_file)[0]):
+                os.makedirs(os.path.split(dest_file)[0])
+            shutil.copyfile(os.path.join(
+                AppLocation.get_section_data_path(
+                    u'servicemanager'), bga),
+                dest_file)
+            song.media_files.append(MediaFile.populate(
+                weight=i, file_name=dest_file))
+        self.plugin.manager.save_object(song, True)
+
     def addEndHeaderBar(self):
         self.addToolbarSeparator()
         ## Song Maintenance Button ##
@@ -210,7 +229,7 @@
             search_results = self.plugin.manager.get_all_objects(Song,
                 Song.theme_name.like(u'%' + search_keywords + u'%'))
             self.displayResultsSong(search_results)
-        self.check_search_result()
+        self.checkSearchResult()
 
     def searchEntire(self, search_keywords):
         return self.plugin.manager.get_all_objects(Song,
@@ -244,7 +263,7 @@
 
     def displayResultsSong(self, searchresults):
         log.debug(u'display results Song')
-        self.save_auto_select_id()
+        self.saveAutoSelectId()
         self.listView.clear()
         # Sort the songs by its title considering language specific characters.
         # lower() is needed for windows!
@@ -258,9 +277,9 @@
             song_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(song.id))
             self.listView.addItem(song_name)
             # Auto-select the item if name has been set
-            if song.id == self.auto_select_id:
+            if song.id == self.autoSelectId:
                 self.listView.setCurrentItem(song_name)
-        self.auto_select_id = -1
+        self.autoSelectId = -1
 
     def displayResultsAuthor(self, searchresults):
         log.debug(u'display results Author')
@@ -312,7 +331,7 @@
         self.edit_song_form.exec_()
         self.onClearTextButtonClick()
         self.onSelectionChange()
-        self.auto_select_id = -1
+        self.autoSelectId = -1
 
     def onSongMaintenanceClick(self):
         self.song_maintenance_form.exec_()
@@ -335,9 +354,9 @@
         if valid:
             self.remoteSong = song_id
             self.remoteTriggered = remote_type
-            self.edit_song_form.loadSong(song_id, (remote_type == u'P'))
+            self.edit_song_form.loadSong(song_id, remote_type == u'P')
             self.edit_song_form.exec_()
-            self.auto_select_id = -1
+            self.autoSelectId = -1
             self.onSongListLoad()
 
     def onEditClick(self):
@@ -350,7 +369,7 @@
             item_id = (self.editItem.data(QtCore.Qt.UserRole)).toInt()[0]
             self.edit_song_form.loadSong(item_id, False)
             self.edit_song_form.exec_()
-            self.auto_select_id = -1
+            self.autoSelectId = -1
             self.onSongListLoad()
         self.editItem = None
 
@@ -395,12 +414,12 @@
     def generateSlideData(self, service_item, item=None, xmlVersion=False):
         log.debug(u'generateSlideData (%s:%s)' % (service_item, item))
         item_id = self._getIdOfItemToGenerate(item, self.remoteSong)
-        service_item.add_capability(ItemCapabilities.AllowsEdit)
-        service_item.add_capability(ItemCapabilities.AllowsPreview)
-        service_item.add_capability(ItemCapabilities.AllowsLoop)
+        service_item.add_capability(ItemCapabilities.CanEdit)
+        service_item.add_capability(ItemCapabilities.CanPreview)
+        service_item.add_capability(ItemCapabilities.CanLoop)
         service_item.add_capability(ItemCapabilities.OnLoadUpdate)
         service_item.add_capability(ItemCapabilities.AddIfNewItem)
-        service_item.add_capability(ItemCapabilities.AllowsVirtualSplit)
+        service_item.add_capability(ItemCapabilities.CanSoftBreak)
         song = self.plugin.manager.get_object(Song, item_id)
         service_item.theme = song.theme_name
         service_item.edit_id = item_id
@@ -471,6 +490,10 @@
         service_item.data_string = {u'title': song.search_title,
             u'authors': u', '.join(author_list)}
         service_item.xml_version = self.openLyrics.song_to_xml(song)
+        # Add the audio file to the service item.
+        if len(song.media_files) > 0:
+            service_item.add_capability(ItemCapabilities.HasBackgroundAudio)
+            service_item.background_audio = [m.file_name for m in song.media_files]
         return True
 
     def serviceLoad(self, item):
@@ -510,8 +533,15 @@
                     add_song = False
                     editId = song.id
                     break
+                # If there's any backing tracks, copy them over.
+                if len(item.background_audio) > 0:
+                    self._updateBackgroundAudio(song, item)
         if add_song and self.addSongFromService:
-            editId = self.openLyrics.xml_to_song(item.xml_version)
+            song = self.openLyrics.xml_to_song(item.xml_version)
+            # If there's any backing tracks, copy them over.
+            if len(item.background_audio) > 0:
+                self._updateBackgroundAudio(song, item)
+            editId = song.id
             self.onSearchTextButtonClick()
         # Update service with correct song id.
         if editId:

=== modified file 'openlp/plugins/songs/lib/xml.py'
--- openlp/plugins/songs/lib/xml.py	2011-08-14 10:56:05 +0000
+++ openlp/plugins/songs/lib/xml.py	2011-09-04 12:16:24 +0000
@@ -343,7 +343,7 @@
         self._process_topics(properties, song)
         clean_song(self.manager, song)
         self.manager.save_object(song)
-        return song.id
+        return song
 
     def _add_text_to_element(self, tag, parent, text=None, label=None):
         if label:

=== added file 'resources/forms/mediafilesdialog.ui'
--- resources/forms/mediafilesdialog.ui	1970-01-01 00:00:00 +0000
+++ resources/forms/mediafilesdialog.ui	2011-09-04 12:16:24 +0000
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MediaFilesDialog</class>
+ <widget class="QDialog" name="MediaFilesDialog">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Select Media File(s)</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="filesVerticalLayout">
+   <property name="spacing">
+    <number>8</number>
+   </property>
+   <property name="margin">
+    <number>8</number>
+   </property>
+   <item>
+    <widget class="QLabel" name="selectLabel">
+     <property name="text">
+      <string>Select one or more audio files from the list below, and click OK to import them into this song.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QListView" name="fileListView">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="../images/openlp-2.qrc"/>
+ </resources>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>MediaFilesDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>MediaFilesDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>


Follow ups