openlp/openlp/plugins/songs/lib/__init__.py

646 lines
25 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2013-01-06 17:25:49 +00:00
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2013-12-24 08:56:50 +00:00
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
2012-11-11 21:16:14 +00:00
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
2012-10-21 13:16:22 +00:00
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
2013-02-11 19:44:04 +00:00
"""
The :mod:`~openlp.plugins.songs.lib` module contains a number of library functions and classes used in the Songs plugin.
"""
2013-06-14 20:20:26 +00:00
import logging
import os
2011-03-14 18:59:59 +00:00
import re
2013-01-18 23:31:02 +00:00
from PyQt4 import QtGui
2011-02-23 20:22:29 +00:00
2013-10-13 15:52:04 +00:00
from openlp.core.common import AppLocation
from openlp.core.lib import translate
2013-10-13 13:51:13 +00:00
from openlp.core.utils import CONTROL_CHARS
2013-06-14 20:20:26 +00:00
from openlp.plugins.songs.lib.db import MediaFile, Song
2013-08-31 18:17:38 +00:00
from .db import Author
from .ui import SongStrings
2013-06-14 20:20:26 +00:00
log = logging.getLogger(__name__)
2011-06-01 22:47:42 +00:00
WHITESPACE = re.compile(r'[\W_]+', re.UNICODE)
2013-08-31 18:17:38 +00:00
APOSTROPHE = re.compile('[\'`ʻ]', re.UNICODE)
# PATTERN will look for the next occurence of one of these symbols:
# \controlword - optionally preceded by \*, optionally followed by a number
# \'## - where ## is a pair of hex digits, representing a single character
# \# - where # is a single non-alpha character, representing a special symbol
# { or } - marking the beginning/end of a group
# a run of characters without any \ { } or end-of-line
2013-09-08 03:50:51 +00:00
PATTERN = re.compile(r"(\\\*)?\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z*])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
2012-07-03 22:26:54 +00:00
# RTF control words which specify a "destination" to be ignored.
DESTINATIONS = frozenset((
2013-08-31 18:17:38 +00:00
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor',
'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref', 'atntime',
'atrfend', 'atrfstart', 'author', 'background', 'bkmkend',
'bkmkstart', 'blipuid', 'buptim', 'category',
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim',
'datafield', 'datastore', 'defchp', 'defpap', 'do', 'doccomm',
'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname',
'falt', 'fchars', 'ffdeftext', 'ffentrymcr', 'ffexitmcr',
2013-09-08 03:50:51 +00:00
'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext',
'file', 'filetbl', 'fldinst', 'fldtype', 'fname',
2013-08-31 18:17:38 +00:00
'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr',
'footnote', 'formfield', 'ftncn', 'ftnsep', 'ftnsepc', 'g',
'generator', 'gridtbl', 'header', 'headerf', 'headerl',
'headerr', 'hl', 'hlfr', 'hlinkbase', 'hlloc', 'hlsrc', 'hsv',
'htmltag', 'info', 'keycode', 'keywords', 'latentstyles',
'lchars', 'levelnumbers', 'leveltext', 'lfolevel', 'linkval',
'list', 'listlevel', 'listname', 'listoverride',
'listoverridetable', 'listpicture', 'liststylename', 'listtable',
'listtext', 'lsdlockedexcept', 'macc', 'maccPr', 'mailmerge',
'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr',
'mbaseJc', 'mbegChr', 'mborderBox', 'mborderBoxPr', 'mbox',
'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide',
'mden', 'mdiff', 'mdPr', 'me', 'mendChr', 'meqArr', 'meqArrPr',
'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr',
'mgroupChrPr', 'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight',
'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow',
'mlimlowPr', 'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname',
'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs',
'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname',
'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag',
'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr', 'mnoBreak',
'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr',
'mopEmu', 'mphant', 'mphantPr', 'mplcHide', 'mpos', 'mr',
'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre',
'msPrePr', 'msSub', 'msSubPr', 'msSubSup', 'msSubSupPr', 'msSup',
'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV',
'msub', 'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype',
'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile',
'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops',
'oldsprops', 'oldtprops', 'oleclsid', 'operator', 'panose',
'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict',
'pn', 'pnseclvl', 'pntext', 'pntxta', 'pntxtb', 'printim',
'private', 'propname', 'protend', 'protstart', 'protusertbl',
'pxe', 'result', 'revtbl', 'revtim', 'rsidtbl', 'rxe', 'shp',
'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp',
'staticval', 'stylesheet', 'subject', 'sv', 'svb', 'tc',
'template', 'themedata', 'title', 'txe', 'ud', 'upr',
'userprops', 'wgrffmtfilter', 'windowcaption', 'writereservation',
'writereservhash', 'xe', 'xform', 'xmlattrname', 'xmlattrvalue',
'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'))
2012-07-03 22:26:54 +00:00
# Translation of some special characters.
SPECIAL_CHARS = {
2013-09-08 03:50:51 +00:00
'\n': '\n',
'\r': '\n',
'~': '\u00A0',
'-': '\u00AD',
'_': '\u2011',
2013-08-31 18:17:38 +00:00
'par': '\n',
'sect': '\n\n',
2012-07-03 22:26:54 +00:00
# Required page and column break.
# Would be good if we could split verse into subverses here.
2013-08-31 18:17:38 +00:00
'page': '\n\n',
'column': '\n\n',
2012-07-03 22:26:54 +00:00
# Soft breaks.
2013-08-31 18:17:38 +00:00
'softpage': '[---]',
'softcol': '[---]',
'line': '\n',
'tab': '\t',
'emdash': '\u2014',
2013-12-24 08:56:50 +00:00
'endash': '\u2014',
2013-08-31 18:17:38 +00:00
'emspace': '\u2003',
'enspace': '\u2002',
'qmspace': '\u2005',
'bullet': '\u2022',
'lquote': '\u2018',
'rquote': '\u2019',
'ldblquote': '\u201C',
'rdblquote': '\u201D',
'ltrmark': '\u200E',
'rtlmark': '\u200F',
'zwj': '\u200D',
'zwnj': '\u200C'}
2012-07-03 22:26:54 +00:00
CHARSET_MAPPING = {
2013-09-08 03:50:51 +00:00
'0': 'cp1252',
'128': 'cp932',
'129': 'cp949',
'134': 'cp936',
'161': 'cp1253',
'162': 'cp1254',
'163': 'cp1258',
'177': 'cp1255',
'178': 'cp1256',
'186': 'cp1257',
'204': 'cp1251',
'222': 'cp874',
'238': 'cp1250'}
2012-07-03 22:26:54 +00:00
2011-06-01 22:47:42 +00:00
class VerseType(object):
"""
2013-02-24 18:13:50 +00:00
VerseType provides an enumeration for the tags that may be associated with verses in songs.
"""
Verse = 0
Chorus = 1
Bridge = 2
PreChorus = 3
Intro = 4
Ending = 5
Other = 6
2011-02-17 19:46:01 +00:00
2013-02-24 18:13:50 +00:00
names = [
2013-08-31 18:17:38 +00:00
'Verse',
'Chorus',
'Bridge',
'Pre-Chorus',
'Intro',
'Ending',
'Other']
2013-02-24 18:13:50 +00:00
tags = [name[0].lower() for name in names]
2011-02-17 19:46:01 +00:00
2013-02-24 18:13:50 +00:00
translated_names = [
2012-05-17 18:57:01 +00:00
translate('SongsPlugin.VerseType', 'Verse'),
translate('SongsPlugin.VerseType', 'Chorus'),
translate('SongsPlugin.VerseType', 'Bridge'),
translate('SongsPlugin.VerseType', 'Pre-Chorus'),
translate('SongsPlugin.VerseType', 'Intro'),
translate('SongsPlugin.VerseType', 'Ending'),
translate('SongsPlugin.VerseType', 'Other')]
2013-03-21 19:05:40 +00:00
2013-02-27 12:41:04 +00:00
translated_tags = [name[0].lower() for name in translated_names]
2011-02-16 18:37:51 +00:00
@staticmethod
def translated_tag(verse_tag, default=Other):
"""
2013-02-24 18:13:50 +00:00
Return the translated UPPERCASE tag for a given tag, used to show translated verse tags in UI
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
2013-02-24 18:13:50 +00:00
for num, tag in enumerate(VerseType.tags):
if verse_tag == tag:
2013-02-27 12:41:04 +00:00
return VerseType.translated_tags[num].upper()
2013-03-07 12:34:35 +00:00
if len(VerseType.names) > default:
2013-02-27 12:41:04 +00:00
return VerseType.translated_tags[default].upper()
else:
2013-03-04 08:53:43 +00:00
return VerseType.translated_tags[VerseType.Other].upper()
@staticmethod
def translated_name(verse_tag, default=Other):
"""
Return the translated name for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
2013-02-24 18:13:50 +00:00
for num, tag in enumerate(VerseType.tags):
if verse_tag == tag:
2013-03-07 12:34:35 +00:00
return VerseType.translated_names[num]
if len(VerseType.names) > default:
2013-02-24 18:13:50 +00:00
return VerseType.translated_names[default]
else:
2013-03-04 08:53:43 +00:00
return VerseType.translated_names[VerseType.Other]
@staticmethod
def from_tag(verse_tag, default=Other):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
2013-02-24 18:13:50 +00:00
for num, tag in enumerate(VerseType.tags):
if verse_tag == tag:
return num
2013-03-07 12:34:35 +00:00
if len(VerseType.names) > default:
return default
else:
return VerseType.Other
2011-02-16 18:37:51 +00:00
@staticmethod
def from_translated_tag(verse_tag, default=Other):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
2013-02-24 18:13:50 +00:00
for num, tag in enumerate(VerseType.translated_tags):
if verse_tag == tag:
return num
2013-03-07 12:34:35 +00:00
if len(VerseType.names) > default:
return default
else:
return VerseType.Other
@staticmethod
def from_string(verse_name, default=Other):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
verse_name = verse_name.lower()
2013-02-24 18:13:50 +00:00
for num, name in enumerate(VerseType.names):
if verse_name == name.lower():
return num
return default
2011-02-16 18:37:51 +00:00
@staticmethod
def from_translated_string(verse_name):
2011-02-16 18:37:51 +00:00
"""
Return the VerseType for a given string
``verse_name``
2011-02-16 18:37:51 +00:00
The string to return a VerseType for
"""
verse_name = verse_name.lower()
2013-02-24 18:13:50 +00:00
for num, translation in enumerate(VerseType.translated_names):
if verse_name == translation.lower():
2011-02-16 18:37:51 +00:00
return num
@staticmethod
def from_loose_input(verse_name, default=Other):
"""
2011-11-24 22:49:21 +00:00
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
"""
if len(verse_name) > 1:
verse_index = VerseType.from_translated_string(verse_name)
if verse_index is None:
verse_index = VerseType.from_string(verse_name, default)
elif len(verse_name) == 1:
verse_index = VerseType.from_translated_tag(verse_name, None)
if verse_index is None:
verse_index = VerseType.from_tag(verse_name, default)
else:
return default
return verse_index
def retrieve_windows_encoding(recommendation=None):
2011-02-07 16:29:06 +00:00
"""
2013-02-24 18:13:50 +00:00
Determines which encoding to use on an information source. The process uses both automated detection, which is
passed to this method as a recommendation, and user confirmation to return an encoding.
2011-02-07 16:29:06 +00:00
``recommendation``
2013-02-24 18:13:50 +00:00
A recommended encoding discovered programmatically for the user to confirm.
2011-02-07 16:29:06 +00:00
"""
# map chardet result to compatible windows standard code page
2013-08-31 18:17:38 +00:00
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874',
'SHIFT_JIS': 'cp932', 'GB2312': 'cp936', 'HZ-GB-2312': 'cp936',
'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
'windows-1250': 'cp1250', 'windows-1251': 'cp1251',
'windows-1252': 'cp1252', 'ISO-8859-7': 'cp1253',
'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
'windows-1255': 'cp1255'}
if recommendation in codepage_mapping:
recommendation = codepage_mapping[recommendation]
# Show dialog for encoding selection
2013-08-31 18:17:38 +00:00
encodings = [('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
('cp1257', translate('SongsPlugin', 'Baltic (CP-1257)')),
('cp1250', translate('SongsPlugin', 'Central European (CP-1250)')),
('cp1251', translate('SongsPlugin', 'Cyrillic (CP-1251)')),
('cp1253', translate('SongsPlugin', 'Greek (CP-1253)')),
('cp1255', translate('SongsPlugin', 'Hebrew (CP-1255)')),
('cp932', translate('SongsPlugin', 'Japanese (CP-932)')),
('cp949', translate('SongsPlugin', 'Korean (CP-949)')),
('cp936', translate('SongsPlugin', 'Simplified Chinese (CP-936)')),
('cp874', translate('SongsPlugin', 'Thai (CP-874)')),
('cp950', translate('SongsPlugin', 'Traditional Chinese (CP-950)')),
('cp1254', translate('SongsPlugin', 'Turkish (CP-1254)')),
('cp1258', translate('SongsPlugin', 'Vietnam (CP-1258)')),
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))]
recommended_index = -1
if recommendation:
for index in range(len(encodings)):
if recommendation == encodings[index][0]:
recommended_index = index
break
2013-09-08 03:50:51 +00:00
if recommended_index > -1:
choice = QtGui.QInputDialog.getItem(None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'The codepage setting is responsible\n'
2013-01-06 17:25:49 +00:00
'for the correct character representation.\nUsually you are fine with the preselected choice.'),
[pair[1] for pair in encodings], recommended_index, False)
else:
choice = QtGui.QInputDialog.getItem(None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'Please choose the character encoding.\n'
2013-01-06 17:25:49 +00:00
'The encoding is responsible for the correct character representation.'),
[pair[1] for pair in encodings], 0, False)
if not choice[1]:
return None
2013-09-08 03:50:51 +00:00
return next(filter(lambda item: item[1] == choice[0], encodings))[0]
2011-06-01 22:47:42 +00:00
def clean_string(string):
"""
2013-02-24 18:13:50 +00:00
Strips punctuation from the passed string to assist searching.
2011-06-01 22:47:42 +00:00
"""
2013-08-31 18:17:38 +00:00
return WHITESPACE.sub(' ', APOSTROPHE.sub('', string)).lower()
2012-04-04 07:26:51 +00:00
2012-03-22 06:50:54 +00:00
def clean_title(title):
"""
2013-02-24 18:13:50 +00:00
Cleans the song title by removing Unicode control chars groups C0 & C1, as well as any trailing spaces.
2012-03-22 06:50:54 +00:00
"""
2013-08-31 18:17:38 +00:00
return CONTROL_CHARS.sub('', title).rstrip()
2011-06-01 22:47:42 +00:00
2011-03-14 18:59:59 +00:00
def clean_song(manager, song):
2011-02-23 20:22:29 +00:00
"""
2013-02-24 18:13:50 +00:00
Cleans the search title, rebuilds the search lyrics, adds a default author if the song does not have one and other
clean ups. This should always called when a new song is added or changed.
2011-02-23 20:22:29 +00:00
``manager``
The song's manager.
``song``
The song object.
"""
2013-08-31 18:17:38 +00:00
from .xml import SongXML
2012-03-22 06:50:54 +00:00
if song.title:
song.title = clean_title(song.title)
else:
2013-08-31 18:17:38 +00:00
song.title = ''
2012-03-22 06:50:54 +00:00
if song.alternate_title:
song.alternate_title = clean_title(song.alternate_title)
else:
2013-08-31 18:17:38 +00:00
song.alternate_title = ''
song.search_title = clean_string(song.title) + '@' + clean_string(song.alternate_title)
2011-05-28 17:01:41 +00:00
# Only do this, if we the song is a 1.9.4 song (or older).
2013-08-31 18:17:38 +00:00
if song.lyrics.find('<lyrics language="en">') != -1:
2013-02-24 18:13:50 +00:00
# Remove the old "language" attribute from lyrics tag (prior to 1.9.5). This is not very important, but this
# keeps the database clean. This can be removed when everybody has cleaned his songs.
2013-08-31 18:17:38 +00:00
song.lyrics = song.lyrics.replace('<lyrics language="en">', '<lyrics>')
2011-05-28 17:01:41 +00:00
verses = SongXML().get_verses(song.lyrics)
2013-08-31 18:17:38 +00:00
song.search_lyrics = ' '.join([clean_string(verse[1])
2011-06-01 22:47:42 +00:00
for verse in verses])
2011-05-28 17:01:41 +00:00
# We need a new and clean SongXML instance.
sxml = SongXML()
2013-02-24 18:13:50 +00:00
# Rebuild the song's verses, to remove any wrong verse names (for example translated ones), which might have
# been added prior to 1.9.5.
2011-05-28 17:01:41 +00:00
# List for later comparison.
compare_order = []
for verse in verses:
2013-08-31 18:17:38 +00:00
verse_type = VerseType.tags[VerseType.from_loose_input(verse[0]['type'])]
2011-05-28 17:01:41 +00:00
sxml.add_verse_to_lyrics(
verse_type,
2013-08-31 18:17:38 +00:00
verse[0]['label'],
2011-05-28 17:01:41 +00:00
verse[1],
2013-08-31 18:17:38 +00:00
verse[0].get('lang')
2011-05-28 17:01:41 +00:00
)
2013-08-31 18:17:38 +00:00
compare_order.append(('%s%s' % (verse_type, verse[0]['label'])).upper())
if verse[0]['label'] == '1':
2011-05-28 17:01:41 +00:00
compare_order.append(verse_type.upper())
2013-08-31 18:17:38 +00:00
song.lyrics = str(sxml.extract_xml(), 'utf-8')
2013-02-24 18:13:50 +00:00
# Rebuild the verse order, to convert translated verse tags, which might have been added prior to 1.9.5.
2011-05-28 17:01:41 +00:00
if song.verse_order:
2013-08-31 18:17:38 +00:00
order = CONTROL_CHARS.sub('', song.verse_order).strip().split()
2011-05-28 17:01:41 +00:00
else:
order = []
new_order = []
for verse_def in order:
2013-02-24 18:13:50 +00:00
verse_type = VerseType.tags[
2011-05-28 17:01:41 +00:00
VerseType.from_loose_input(verse_def[0])]
if len(verse_def) > 1:
2013-08-31 18:17:38 +00:00
new_order.append(('%s%s' % (verse_type, verse_def[1:])).upper())
2011-05-28 17:01:41 +00:00
else:
new_order.append(verse_type.upper())
2013-08-31 18:17:38 +00:00
song.verse_order = ' '.join(new_order)
2011-05-28 17:01:41 +00:00
# Check if the verse order contains tags for verses which do not exist.
for order in new_order:
if order not in compare_order:
2013-08-31 18:17:38 +00:00
song.verse_order = ''
2011-05-28 17:01:41 +00:00
break
2011-06-01 22:47:42 +00:00
else:
verses = SongXML().get_verses(song.lyrics)
2013-08-31 18:17:38 +00:00
song.search_lyrics = ' '.join([clean_string(verse[1])
2011-06-01 22:47:42 +00:00
for verse in verses])
2011-03-15 17:52:42 +00:00
# The song does not have any author, add one.
if not song.authors:
name = SongStrings.AuthorUnknown
2013-01-06 17:25:49 +00:00
author = manager.get_object_filtered(Author, Author.display_name == name)
2011-03-15 17:52:42 +00:00
if author is None:
2013-08-31 18:17:38 +00:00
author = Author.populate(display_name=name, last_name='', first_name='')
2011-03-15 17:52:42 +00:00
song.authors.append(author)
if song.copyright:
2013-08-31 18:17:38 +00:00
song.copyright = CONTROL_CHARS.sub('', song.copyright).strip()
2011-02-23 20:22:29 +00:00
2012-07-03 22:26:54 +00:00
def get_encoding(font, font_table, default_encoding, failed=False):
"""
Finds an encoding to use. Asks user, if necessary.
``font``
2013-02-24 18:13:50 +00:00
The number of currently active font.
2012-07-03 22:26:54 +00:00
``font_table``
2013-02-24 18:13:50 +00:00
Dictionary of fonts and respective encodings.
2012-07-03 22:26:54 +00:00
``default_encoding``
2013-02-24 18:13:50 +00:00
The default encoding to use when font_table is empty or no font is used.
2012-07-03 22:26:54 +00:00
``failed``
2013-02-24 18:13:50 +00:00
A boolean indicating whether the previous encoding didn't work.
2012-07-03 22:26:54 +00:00
"""
encoding = None
if font in font_table:
encoding = font_table[font]
if not encoding and default_encoding:
encoding = default_encoding
if not encoding or failed:
encoding = retrieve_windows_encoding()
default_encoding = encoding
font_table[font] = encoding
return encoding, default_encoding
def strip_rtf(text, default_encoding=None):
"""
2012-07-03 22:26:54 +00:00
This function strips RTF control structures and returns an unicode string.
Thanks to Markus Jarderot (MizardX) for this code, used by permission.
http://stackoverflow.com/questions/188545
2012-07-03 22:26:54 +00:00
``text``
2013-02-24 18:13:50 +00:00
RTF-encoded text, a string.
2012-07-03 22:26:54 +00:00
``default_encoding``
2013-02-24 18:13:50 +00:00
Default encoding to use when no encoding is specified.
"""
2012-07-03 22:26:54 +00:00
# Current font is the font tag we last met.
2013-08-31 18:17:38 +00:00
font = ''
2012-07-03 22:26:54 +00:00
# Character encoding is defined inside fonttable.
# font_table could contain eg u'0': u'cp1252'
2013-08-31 18:17:38 +00:00
font_table = {'': ''}
2012-07-03 22:26:54 +00:00
# Stack of things to keep track of when entering/leaving groups.
stack = []
# Whether this group (and all inside it) are "ignorable".
ignorable = False
# Number of ASCII characters to skip after an unicode character.
ucskip = 1
# Number of ASCII characters left to skip.
curskip = 0
# Output buffer.
out = []
2013-09-08 03:50:51 +00:00
# Encoded buffer.
ebytes = bytearray()
2012-07-03 22:26:54 +00:00
for match in PATTERN.finditer(text):
2013-09-08 03:50:51 +00:00
iinu, word, arg, hex, char, brace, tchar = match.groups()
# \x (non-alpha character)
if char:
if char in '\\{}':
tchar = char
else:
word = char
# Flush encoded buffer to output buffer
if ebytes and not hex and not tchar:
failed = False
while True:
try:
encoding, default_encoding = get_encoding(font, font_table, default_encoding, failed=failed)
if not encoding:
return None
dbytes = ebytes.decode(encoding)
# Code 5C is a peculiar case with Windows Codepage 932
if encoding == 'cp932' and '\\' in dbytes:
dbytes = dbytes.replace('\\', '\u00A5')
out.append(dbytes)
ebytes.clear()
except UnicodeDecodeError:
failed = True
else:
break
# {}
2012-07-03 22:26:54 +00:00
if brace:
curskip = 0
2013-08-31 18:17:38 +00:00
if brace == '{':
2012-07-03 22:26:54 +00:00
# Push state
stack.append((ucskip, ignorable, font))
2013-09-08 03:50:51 +00:00
elif brace == '}' and len(stack) > 0:
2012-07-03 22:26:54 +00:00
# Pop state
ucskip, ignorable, font = stack.pop()
# \command
elif word:
curskip = 0
if word in DESTINATIONS:
ignorable = True
elif word in SPECIAL_CHARS:
2013-09-08 03:50:51 +00:00
if not ignorable:
out.append(SPECIAL_CHARS[word])
2013-08-31 18:17:38 +00:00
elif word == 'uc':
2012-07-03 22:26:54 +00:00
ucskip = int(arg)
2013-09-08 03:50:51 +00:00
elif word == 'u':
2012-07-03 22:26:54 +00:00
c = int(arg)
if c < 0:
c += 0x10000
2013-09-08 03:50:51 +00:00
if not ignorable:
out.append(chr(c))
2012-07-03 22:26:54 +00:00
curskip = ucskip
2013-08-31 18:17:38 +00:00
elif word == 'fonttbl':
2012-07-03 22:26:54 +00:00
ignorable = True
2013-08-31 18:17:38 +00:00
elif word == 'f':
2012-07-03 22:26:54 +00:00
font = arg
2013-08-31 18:17:38 +00:00
elif word == 'ansicpg':
2012-07-03 22:26:54 +00:00
font_table[font] = 'cp' + arg
2013-09-08 03:50:51 +00:00
elif word == 'fcharset' and font not in font_table and arg in CHARSET_MAPPING:
font_table[font] = CHARSET_MAPPING[arg]
elif word == 'fldrslt':
pass
# \* 'Ignore if not understood' marker
elif iinu:
ignorable = True
2012-07-03 22:26:54 +00:00
# \'xx
elif hex:
if curskip > 0:
curskip -= 1
elif not ignorable:
2013-09-08 03:50:51 +00:00
ebytes.append(int(hex, 16))
2012-07-03 22:26:54 +00:00
elif tchar:
if curskip > 0:
curskip -= 1
elif not ignorable:
2013-09-08 03:50:51 +00:00
ebytes += tchar.encode()
2013-08-31 18:17:38 +00:00
text = ''.join(out)
2012-07-03 22:26:54 +00:00
return text, default_encoding
2013-06-14 20:20:26 +00:00
def delete_song(song_id, song_plugin):
"""
Deletes a song from the database. Media files associated to the song
are removed prior to the deletion of the song.
``song_id``
The ID of the song to delete.
``song_plugin``
The song plugin instance.
"""
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
for media_file in media_files:
try:
os.remove(media_file.file_name)
except:
log.exception('Could not remove file: %s', media_file.file_name)
try:
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
if os.path.exists(save_path):
os.rmdir(save_path)
except OSError:
2013-08-31 18:17:38 +00:00
log.exception('Could not remove directory: %s', save_path)
2013-06-14 20:20:26 +00:00
song_plugin.manager.delete_object(Song, song_id)