~ubuntu-branches/debian/jessie/openlp/jessie

« back to all changes in this revision

Viewing changes to openlp/plugins/songs/lib/powersongimport.py

  • Committer: Package Import Robot
  • Author(s): Raoul Snyman
  • Date: 2012-06-23 21:54:30 UTC
  • mfrom: (1.1.1)
  • Revision ID: package-import@ubuntu.com-20120623215430-8qrmxy3d7jwv3jvn
Tags: 1.9.10-1
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
 
3
 
 
4
###############################################################################
 
5
# OpenLP - Open Source Lyrics Projection                                      #
 
6
# --------------------------------------------------------------------------- #
 
7
# Copyright (c) 2008-2012 Raoul Snyman                                        #
 
8
# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan      #
 
9
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
 
10
# Meinert Jordan, Armin Köhler, Edwin Lunando, Joshua Miller, Stevan Pettit,  #
 
11
# Andreas Preikschat, Mattias Põldaru, Christian Richter, Philip Ridout,      #
 
12
# Simon Scudder, Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon      #
 
13
# Tibble, Dave Warnock, Frode Woldsund                                        #
 
14
# --------------------------------------------------------------------------- #
 
15
# This program is free software; you can redistribute it and/or modify it     #
 
16
# under the terms of the GNU General Public License as published by the Free  #
 
17
# Software Foundation; version 2 of the License.                              #
 
18
#                                                                             #
 
19
# This program is distributed in the hope that it will be useful, but WITHOUT #
 
20
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
 
21
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
 
22
# more details.                                                               #
 
23
#                                                                             #
 
24
# You should have received a copy of the GNU General Public License along     #
 
25
# with this program; if not, write to the Free Software Foundation, Inc., 59  #
 
26
# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 
27
###############################################################################
 
28
"""
 
29
The :mod:`powersongimport` module provides the functionality for importing
 
30
PowerSong songs into the OpenLP database.
 
31
"""
 
32
import logging
 
33
import fnmatch
 
34
import os
 
35
 
 
36
from openlp.core.lib import translate
 
37
from openlp.plugins.songs.lib.songimport import SongImport
 
38
 
 
39
log = logging.getLogger(__name__)
 
40
 
 
41
class PowerSongImport(SongImport):
 
42
    """
 
43
    The :class:`PowerSongImport` class provides the ability to import song files
 
44
    from PowerSong.
 
45
 
 
46
    **PowerSong 1.0 Song File Format:**
 
47
 
 
48
    The file has a number of label-field (think key-value) pairs.
 
49
 
 
50
    Label and Field strings:
 
51
 
 
52
        * Every label and field is a variable length string preceded by an
 
53
          integer specifying it's byte length.
 
54
        * Integer is 32-bit but is encoded in 7-bit format to save space. Thus
 
55
          if length will fit in 7 bits (ie <= 127) it takes up only one byte.
 
56
 
 
57
    Metadata fields:
 
58
 
 
59
        * Every PowerSong file has a TITLE field.
 
60
        * There is zero or more AUTHOR fields.
 
61
        * There is always a COPYRIGHTLINE label, but its field may be empty.
 
62
          This field may also contain a CCLI number: e.g. "CCLI 176263".
 
63
 
 
64
    Lyrics fields:
 
65
 
 
66
        * Each verse is contained in a PART field.
 
67
        * Lines have Windows line endings ``CRLF`` (0x0d, 0x0a).
 
68
        * There is no concept of verse types.
 
69
 
 
70
    Valid extensions for a PowerSong song file are:
 
71
 
 
72
        * .song
 
73
    """
 
74
    @staticmethod
 
75
    def isValidSource(import_source):
 
76
        """
 
77
        Checks if source is a PowerSong 1.0 folder:
 
78
            * is a directory
 
79
            * contains at least one *.song file
 
80
        """
 
81
        if os.path.isdir(import_source):
 
82
            for file in os.listdir(import_source):
 
83
                if fnmatch.fnmatch(file, u'*.song'):
 
84
                    return True
 
85
        return False
 
86
 
 
87
    def doImport(self):
 
88
        """
 
89
        Receive either a list of files or a folder (unicode) to import.
 
90
        """
 
91
        from importer import SongFormat
 
92
        PS_string = SongFormat.get(SongFormat.PowerSong, u'name')
 
93
        if isinstance(self.importSource, unicode):
 
94
            if os.path.isdir(self.importSource):
 
95
                dir = self.importSource
 
96
                self.importSource = []
 
97
                for file in os.listdir(dir):
 
98
                    if fnmatch.fnmatch(file, u'*.song'):
 
99
                        self.importSource.append(os.path.join(dir, file))
 
100
            else:
 
101
                self.importSource = u''
 
102
        if not self.importSource or not isinstance(self.importSource, list):
 
103
            self.logError(unicode(translate('SongsPlugin.PowerSongImport',
 
104
                'No songs to import.')),
 
105
                unicode(translate('SongsPlugin.PowerSongImport',
 
106
                'No %s files found.' % PS_string)))
 
107
            return
 
108
        self.importWizard.progressBar.setMaximum(len(self.importSource))
 
109
        for file in self.importSource:
 
110
            if self.stopImportFlag:
 
111
                return
 
112
            self.setDefaults()
 
113
            parse_error = False
 
114
            with open(file, 'rb') as song_data:
 
115
                while True:
 
116
                    try:
 
117
                        label = self._readString(song_data)
 
118
                        if not label:
 
119
                            break
 
120
                        field = self._readString(song_data)
 
121
                    except ValueError:
 
122
                        parse_error = True
 
123
                        self.logError(os.path.basename(file), unicode(
 
124
                            translate('SongsPlugin.PowerSongImport',
 
125
                            'Invalid %s file. Unexpected byte value.'
 
126
                            % PS_string)))
 
127
                        break
 
128
                    else:
 
129
                        if label == u'TITLE':
 
130
                            self.title = field.replace(u'\n', u' ')
 
131
                        elif label == u'AUTHOR':
 
132
                            self.parseAuthor(field)
 
133
                        elif label == u'COPYRIGHTLINE':
 
134
                            found_copyright = True
 
135
                            self._parseCopyrightCCLI(field)
 
136
                        elif label == u'PART':
 
137
                            self.addVerse(field)
 
138
            if parse_error:
 
139
                continue
 
140
            # Check that file had TITLE field
 
141
            if not self.title:
 
142
                self.logError(os.path.basename(file), unicode(
 
143
                    translate('SongsPlugin.PowerSongImport',
 
144
                    'Invalid %s file. Missing "TITLE" header.' % PS_string)))
 
145
                continue
 
146
            # Check that file had COPYRIGHTLINE label
 
147
            if not found_copyright:
 
148
                self.logError(self.title, unicode(
 
149
                    translate('SongsPlugin.PowerSongImport',
 
150
                    'Invalid %s file. Missing "COPYRIGHTLINE" '
 
151
                    'header.' % PS_string)))
 
152
                continue
 
153
            # Check that file had at least one verse
 
154
            if not self.verses:
 
155
                self.logError(self.title, unicode(
 
156
                    translate('SongsPlugin.PowerSongImport',
 
157
                    'Verses not found. Missing "PART" header.')))
 
158
                continue
 
159
            if not self.finish():
 
160
                self.logError(self.title)
 
161
 
 
162
    def _readString(self, file_object):
 
163
        """
 
164
        Reads in next variable-length string.
 
165
        """
 
166
        string_len = self._read7BitEncodedInteger(file_object)
 
167
        return unicode(file_object.read(string_len), u'utf-8', u'ignore')
 
168
 
 
169
    def _read7BitEncodedInteger(self, file_object):
 
170
        """
 
171
        Reads in a 32-bit integer in compressed 7-bit format.
 
172
 
 
173
        Accomplished by reading the integer 7 bits at a time. The high bit
 
174
        of the byte when set means to continue reading more bytes.
 
175
        If the integer will fit in 7 bits (ie <= 127), it only takes up one
 
176
        byte. Otherwise, it may take up to 5 bytes.
 
177
 
 
178
        Reference: .NET method System.IO.BinaryReader.Read7BitEncodedInt
 
179
        """
 
180
        val = 0
 
181
        shift = 0
 
182
        i = 0
 
183
        while True:
 
184
            # Check for corrupted stream (since max 5 bytes per 32-bit integer)
 
185
            if i == 5:
 
186
                raise ValueError
 
187
            byte = self._readByte(file_object)
 
188
            # Strip high bit and shift left
 
189
            val += (byte & 0x7f) << shift
 
190
            shift += 7
 
191
            high_bit_set = byte & 0x80
 
192
            if not high_bit_set:
 
193
                break
 
194
            i += 1
 
195
        return val
 
196
 
 
197
    def _readByte(self, file_object):
 
198
        """
 
199
        Reads in next byte as an unsigned integer
 
200
 
 
201
        Note: returns 0 at end of file.
 
202
        """
 
203
        byte_str = file_object.read(1)
 
204
        # If read result is empty, then reached end of file
 
205
        if not byte_str:
 
206
            return 0
 
207
        else:
 
208
            return ord(byte_str)
 
209
 
 
210
    def _parseCopyrightCCLI(self, field):
 
211
        """
 
212
        Look for CCLI song number, and get copyright
 
213
        """
 
214
        copyright, sep, ccli_no = field.rpartition(u'CCLI')
 
215
        if not sep:
 
216
            copyright = ccli_no
 
217
            ccli_no = u''
 
218
        if copyright:
 
219
            self.addCopyright(copyright.rstrip(u'\n').replace(u'\n', u' '))
 
220
        if ccli_no:
 
221
            ccli_no = ccli_no.strip(u' :')
 
222
            if ccli_no.isdigit():
 
223
                self.ccliNumber = ccli_no