openlp/openlp/plugins/songs/lib/importers/songimport.py

419 lines
17 KiB
Python
Raw Normal View History

2010-04-01 21:36:03 +00:00
# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-12-31 15:54:46 +00:00
# Copyright (c) 2008-2023 OpenLP Developers #
2019-04-13 13:00:22 +00:00
# ---------------------------------------------------------------------- #
# 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
2012-04-03 17:58:42 +00:00
2010-08-27 15:25:29 +00:00
import logging
2010-06-19 21:54:53 +00:00
import re
from shutil import copyfile
2011-04-18 16:46:22 +00:00
2015-11-07 00:49:40 +00:00
from PyQt5 import QtCore
2010-04-19 18:43:20 +00:00
from openlp.core.common import normalize_str
2017-10-07 07:05:07 +00:00
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.common.path import create_paths
2017-10-07 07:05:07 +00:00
from openlp.core.common.registry import Registry
2017-10-23 22:09:57 +00:00
from openlp.core.widgets.wizard import WizardStrings
2018-10-02 04:39:42 +00:00
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, SongBook, MediaFile, Song, Topic
2014-07-03 16:54:51 +00:00
from openlp.plugins.songs.lib.openlyricsxml import SongXML
2017-12-28 08:08:12 +00:00
from openlp.plugins.songs.lib.ui import SongStrings
2010-06-06 07:28:07 +00:00
2018-10-02 04:39:42 +00:00
2010-08-27 15:25:29 +00:00
log = logging.getLogger(__name__)
2013-12-24 09:24:32 +00:00
2010-08-27 15:25:29 +00:00
class SongImport(QtCore.QObject):
2010-04-01 21:36:03 +00:00
"""
Helper class for import a song from a third party source into OpenLP
This class just takes the raw strings, and will work out for itself
2010-06-06 07:28:07 +00:00
whether the authors etc already exist and add them or refer to them
2010-04-01 21:36:03 +00:00
as necessary
"""
@staticmethod
2014-03-05 18:58:22 +00:00
def is_valid_source(import_source):
"""
Override this method to validate the source prior to import.
"""
return True
2011-02-18 17:34:43 +00:00
def __init__(self, manager, **kwargs):
2010-04-01 21:36:03 +00:00
"""
Initialise and create defaults for properties
2010-06-06 07:28:07 +00:00
2014-03-05 18:58:22 +00:00
:param manager: An instance of a SongManager, through which all database access is performed.
:param kwargs:
2010-04-12 07:22:56 +00:00
"""
self.manager = manager
2011-02-20 00:05:50 +00:00
QtCore.QObject.__init__(self)
2017-09-30 20:16:30 +00:00
if 'file_path' in kwargs:
self.import_source = kwargs['file_path']
elif 'file_paths' in kwargs:
self.import_source = kwargs['file_paths']
elif 'folder_path' in kwargs:
self.import_source = kwargs['folder_path']
else:
2017-09-30 20:16:30 +00:00
raise KeyError('Keyword arguments "file_path[s]" or "folder_path" not supplied.')
2013-03-07 08:05:43 +00:00
log.debug(self.import_source)
self.import_wizard = None
2011-02-18 17:34:43 +00:00
self.song = None
2013-02-07 11:33:47 +00:00
self.stop_import_flag = False
2014-03-05 18:58:22 +00:00
self.set_defaults()
2013-08-31 18:17:38 +00:00
Registry().register_function('openlp_stop_wizard', self.stop_import)
self.settings = Registry().get('settings')
2010-09-01 19:27:45 +00:00
2014-03-05 18:58:22 +00:00
def set_defaults(self):
"""
Create defaults for properties - call this before each song
if importing many songs at once to ensure a clean beginning
"""
2013-08-31 18:17:38 +00:00
self.title = ''
2014-03-05 18:58:22 +00:00
self.song_number = ''
2013-08-31 18:17:38 +00:00
self.alternate_title = ''
self.copyright = ''
self.comments = ''
2014-03-05 18:58:22 +00:00
self.theme_name = ''
self.ccli_number = ''
2010-06-06 07:28:07 +00:00
self.authors = []
self.topics = []
2014-03-05 18:58:22 +00:00
self.media_files = []
self.song_book_name = ''
self.song_book_pub = ''
self.verse_order_list_generated_useful = False
self.verse_order_list_generated = []
self.verse_order_list = []
2010-06-06 07:28:07 +00:00
self.verses = []
2014-03-05 18:58:22 +00:00
self.verse_counts = {}
self.copyright_string = translate('SongsPlugin.SongImport', 'copyright')
2014-03-05 18:58:22 +00:00
def log_error(self, file_path, reason=SongStrings.SongIncomplete):
2011-04-17 15:47:02 +00:00
"""
This should be called, when a song could not be imported.
2014-03-05 18:58:22 +00:00
:param file_path: This should be the file path if ``self.import_source`` is a list with different files. If it
2015-09-08 19:13:59 +00:00
is not a list, but a single file (for instance a database), then this should be the song's title.
2014-03-05 18:58:22 +00:00
:param reason: The reason why the import failed. The string should be as informative as possible.
2011-04-17 15:47:02 +00:00
"""
2014-03-05 18:58:22 +00:00
self.set_defaults()
2013-03-07 08:05:43 +00:00
if self.import_wizard is None:
return
2013-03-07 08:05:43 +00:00
if self.import_wizard.error_report_text_edit.isHidden():
2014-03-05 18:58:22 +00:00
self.import_wizard.error_report_text_edit.setText(
translate('SongsPlugin.SongImport', 'The following songs could not be imported:'))
2013-03-07 08:05:43 +00:00
self.import_wizard.error_report_text_edit.setVisible(True)
self.import_wizard.error_copy_to_button.setVisible(True)
self.import_wizard.error_save_to_button.setVisible(True)
self.import_wizard.error_report_text_edit.append('- {path} ({error})'.format(path=file_path, error=reason))
2011-04-17 15:47:02 +00:00
2013-02-07 11:33:47 +00:00
def stop_import(self):
2010-08-27 15:25:29 +00:00
"""
Sets the flag for importers to stop their import
"""
2013-08-31 18:17:38 +00:00
log.debug('Stopping songs import')
2013-02-07 11:33:47 +00:00
self.stop_import_flag = True
2010-04-12 07:22:56 +00:00
def register(self, import_wizard):
2013-03-07 08:05:43 +00:00
self.import_wizard = import_wizard
2014-03-05 18:58:22 +00:00
def process_song_text(self, text):
2014-03-06 20:40:08 +00:00
"""
Process the song text from import
:param text: Some text
"""
2013-08-31 18:17:38 +00:00
verse_texts = text.split('\n\n')
for verse_text in verse_texts:
2013-08-31 18:17:38 +00:00
if verse_text.strip() != '':
2014-03-05 18:58:22 +00:00
self.process_verse_text(verse_text.strip())
2010-04-12 07:22:56 +00:00
2014-03-05 18:58:22 +00:00
def process_verse_text(self, text):
2014-03-06 20:40:08 +00:00
"""
Process the song verse text from import
:param text: Some text
"""
2013-08-31 18:17:38 +00:00
lines = text.split('\n')
2014-03-05 18:58:22 +00:00
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings.CopyrightSymbol)) >= 0:
2010-04-12 07:22:56 +00:00
copyright_found = False
for line in lines:
2014-03-05 18:58:22 +00:00
if (copyright_found or line.lower().find(self.copyright_string) >= 0 or
2013-08-31 18:17:38 +00:00
line.find(str(SongStrings.CopyrightSymbol)) >= 0):
2010-04-12 07:22:56 +00:00
copyright_found = True
2014-03-05 18:58:22 +00:00
self.add_copyright(line)
2010-04-12 07:22:56 +00:00
else:
2013-03-07 08:05:43 +00:00
self.parse_author(line)
2010-06-06 07:28:07 +00:00
return
2010-04-12 07:22:56 +00:00
if len(lines) == 1:
2013-03-07 08:05:43 +00:00
self.parse_author(lines[0])
2010-04-12 07:22:56 +00:00
return
2010-07-16 21:06:10 +00:00
if not self.title:
self.title = lines[0]
2014-03-05 18:58:22 +00:00
self.add_verse(text)
2010-06-06 07:28:07 +00:00
def parse_song_book_name_and_number(self, book_and_number):
"""
Build the book name and song number from a single string
"""
# Turn 'Spring Harvest 1997 No. 34' or
# 'Spring Harvest 1997 (34)' or
# 'Spring Harvest 1997 34' into
# Book name:'Spring Harvest 1997' and
# Song number: 34
#
# Also, turn 'NRH231.' into
# Book name:'NRH' and
# Song number: 231
book_and_number = book_and_number.strip()
if not book_and_number:
return
book_and_number = book_and_number.replace('No.', ' ')
if ' ' in book_and_number:
parts = book_and_number.split(' ')
self.song_book_name = ' '.join(parts[:-1])
self.song_number = parts[-1].strip('()')
else:
# Something like 'ABC123.'
match = re.match(r'(.*\D)(\d+)', book_and_number)
match_num = re.match(r'(\d+)', book_and_number)
if match:
# Name and number
self.song_book_name = match.group(1)
self.song_number = match.group(2)
# These last two cases aren't tested yet, but
# are here in an attempt to do something vaguely
# sensible if we get a string in a different format
elif match_num:
# Number only
self.song_number = match_num.group(1)
else:
# Name only
self.song_book_name = book_and_number
def add_comment(self, comment):
"""
Build the comments field
"""
if self.comments.find(comment) >= 0:
return
if comment:
self.comments += comment.strip() + '\n'
2014-03-05 18:58:22 +00:00
def add_copyright(self, copyright):
2010-06-06 07:28:07 +00:00
"""
2010-04-01 21:36:03 +00:00
Build the copyright field
2010-04-12 07:22:56 +00:00
"""
if self.copyright.find(copyright) >= 0:
return
if self.copyright:
2010-04-12 07:22:56 +00:00
self.copyright += ' '
self.copyright += copyright
def parse_author(self, text, type=None):
2010-04-12 07:22:56 +00:00
"""
2014-03-06 20:40:08 +00:00
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
2010-04-12 07:22:56 +00:00
"""
2013-08-31 18:17:38 +00:00
for author in text.split(','):
authors = author.split('&')
2010-04-12 07:22:56 +00:00
for i in range(len(authors)):
author2 = authors[i].strip()
2013-08-31 18:17:38 +00:00
if author2.find(' ') == -1 and i < len(authors) - 1:
author2 = author2 + ' ' + authors[i + 1].strip().split(' ')[-1]
if author2.endswith('.'):
2010-04-12 07:22:56 +00:00
author2 = author2[:-1]
if author2:
2017-05-11 20:01:43 +00:00
if type:
self.add_author(author2, type)
else:
self.add_author(author2)
2010-04-12 07:22:56 +00:00
def add_author(self, author, type=None):
2010-06-06 07:28:07 +00:00
"""
2010-04-01 21:36:03 +00:00
Add an author to the list
2010-04-12 07:22:56 +00:00
"""
if (author, type) in self.authors:
2010-04-12 07:22:56 +00:00
return
self.authors.append((author, type))
2010-06-06 07:28:07 +00:00
2017-09-30 21:45:54 +00:00
def add_media_file(self, file_path, weight=0):
"""
Add a media file to the list
"""
2017-09-30 20:16:30 +00:00
if file_path in [x[0] for x in self.media_files]:
return
2017-09-30 20:16:30 +00:00
self.media_files.append((file_path, weight))
2014-03-05 18:58:22 +00:00
def add_verse(self, verse_text, verse_def='v', lang=None):
2010-04-01 21:36:03 +00:00
"""
Add a verse. This is the whole verse, lines split by \\n. It will also
attempt to detect duplicates. In this case it will just add to the verse
order.
2014-03-06 20:40:08 +00:00
:param verse_text: The text of the verse.
:param verse_def: The verse tag can be v1/c1/b etc, or 'v' and 'c' (will count the
2011-01-19 20:08:07 +00:00
verses/choruses itself) or None, where it will assume verse.
2014-03-06 20:40:08 +00:00
:param lang: The language code (ISO-639) of the verse, for example *en* or *de*.
2010-06-06 07:28:07 +00:00
"""
2011-02-18 07:53:40 +00:00
for (old_verse_def, old_verse, old_lang) in self.verses:
if old_verse.strip() == verse_text.strip():
2014-03-05 18:58:22 +00:00
self.verse_order_list_generated.append(old_verse_def)
self.verse_order_list_generated_useful = True
2010-04-03 19:17:37 +00:00
return
2014-03-05 18:58:22 +00:00
if verse_def[0] in self.verse_counts:
self.verse_counts[verse_def[0]] += 1
2010-09-09 19:34:45 +00:00
else:
2014-03-05 18:58:22 +00:00
self.verse_counts[verse_def[0]] = 1
2011-02-18 07:53:40 +00:00
if len(verse_def) == 1:
2014-03-05 18:58:22 +00:00
verse_def += str(self.verse_counts[verse_def[0]])
elif int(verse_def[1:]) > self.verse_counts[verse_def[0]]:
self.verse_counts[verse_def[0]] = int(verse_def[1:])
self.verses.append([verse_def, verse_text.rstrip(), lang])
2013-04-04 16:42:22 +00:00
# A verse_def refers to all verses with that name, adding it once adds every instance, so do not add if already
# used.
2014-03-05 18:58:22 +00:00
if verse_def not in self.verse_order_list_generated:
self.verse_order_list_generated.append(verse_def)
2011-03-19 16:06:04 +00:00
2016-09-25 09:30:00 +00:00
def repeat_verse(self, verse_def=None):
2010-04-01 21:36:03 +00:00
"""
2016-09-25 09:30:00 +00:00
Repeat the verse with the given verse_def or default to repeating the previous verse in the verse order
:param verse_def: verse_def of the verse to be repeated
2010-04-12 07:22:56 +00:00
"""
2014-03-05 18:58:22 +00:00
if self.verse_order_list_generated:
2016-09-25 09:30:00 +00:00
if verse_def:
# If the given verse_def is only one char (like 'v' or 'c'), postfix it with '1'
if len(verse_def) == 1:
verse_def += '1'
if verse_def in self.verse_order_list_generated:
self.verse_order_list_generated.append(verse_def)
else:
log.warning('Trying to add unknown verse_def "%s"' % verse_def)
else:
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
2014-03-05 18:58:22 +00:00
self.verse_order_list_generated_useful = True
2010-04-02 23:24:51 +00:00
2014-03-05 18:58:22 +00:00
def check_complete(self):
2010-04-01 21:36:03 +00:00
"""
Check the mandatory fields are entered (i.e. title and a verse)
2014-03-08 19:58:58 +00:00
Author not checked here, if no author then "Author unknown" is automatically added
2010-04-12 07:22:56 +00:00
"""
if not self.title or not self.verses:
2010-04-12 07:22:56 +00:00
return False
else:
return True
2010-06-06 07:28:07 +00:00
2019-11-09 16:17:41 +00:00
def finish(self, temporary_flag=False):
2010-04-01 21:36:03 +00:00
"""
2011-01-18 04:32:24 +00:00
All fields have been set to this song. Write the song to disk.
2019-11-09 16:17:41 +00:00
:param temporary_flag: should this song be marked as temporary in the db (default=False)
2010-04-12 07:22:56 +00:00
"""
2014-03-05 18:58:22 +00:00
if not self.check_complete():
self.set_defaults()
2011-04-18 16:46:22 +00:00
return False
log.info('committing song {title} to database'.format(title=self.title))
2010-04-12 07:22:56 +00:00
song = Song()
song.title = self.title
2013-03-07 08:05:43 +00:00
if self.import_wizard is not None:
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=song.title))
2013-03-07 08:05:43 +00:00
song.alternate_title = self.alternate_title
2011-03-15 19:33:11 +00:00
# Values will be set when cleaning the song.
2013-08-31 18:17:38 +00:00
song.search_title = ''
song.search_lyrics = ''
song.verse_order = ''
2014-03-05 18:58:22 +00:00
song.song_number = self.song_number
verses_changed_to_other = {}
2011-01-09 16:52:31 +00:00
sxml = SongXML()
other_count = 1
2011-02-18 07:53:40 +00:00
for (verse_def, verse_text, lang) in self.verses:
2013-02-24 18:13:50 +00:00
if verse_def[0].lower() in VerseType.tags:
verse_tag = verse_def[0].lower()
2010-04-12 07:22:56 +00:00
else:
new_verse_def = '{tag}{count:d}'.format(tag=VerseType.tags[VerseType.Other], count=other_count)
2011-02-18 07:53:40 +00:00
verses_changed_to_other[verse_def] = new_verse_def
other_count += 1
2013-02-24 18:13:50 +00:00
verse_tag = VerseType.tags[VerseType.Other]
log.info('Versetype {old} changing to {new}'.format(old=verse_def, new=new_verse_def))
2011-02-18 07:53:40 +00:00
verse_def = new_verse_def
sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], normalize_str(verse_text), lang)
2013-08-31 18:17:38 +00:00
song.lyrics = str(sxml.extract_xml(), 'utf-8')
2014-03-05 18:58:22 +00:00
if not self.verse_order_list and self.verse_order_list_generated_useful:
self.verse_order_list = self.verse_order_list_generated
self.verse_order_list = [verses_changed_to_other.get(v, v) for v in self.verse_order_list]
song.verse_order = ' '.join(self.verse_order_list)
2010-04-12 07:22:56 +00:00
song.copyright = self.copyright
2010-07-20 08:33:22 +00:00
song.comments = self.comments
2014-03-05 18:58:22 +00:00
song.theme_name = self.theme_name
song.ccli_number = self.ccli_number
for author_text, author_type in self.authors:
2014-03-06 20:40:08 +00:00
author = self.manager.get_object_filtered(Author, Author.display_name == author_text)
if not author:
author = Author(display_name=author_text,
last_name=author_text.split(' ')[-1],
first_name=' '.join(author_text.split(' ')[:-1]))
song.add_author(author, author_type)
2014-03-05 18:58:22 +00:00
if self.song_book_name:
song_book = self.manager.get_object_filtered(SongBook, SongBook.name == self.song_book_name)
2010-04-12 07:22:56 +00:00
if song_book is None:
song_book = SongBook(name=self.song_book_name, publisher=self.song_book_pub)
song.add_songbook_entry(song_book, song.song_number)
2014-03-06 20:40:08 +00:00
for topic_text in self.topics:
if not topic_text:
2010-07-15 20:27:44 +00:00
continue
2014-03-06 20:40:08 +00:00
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
2010-04-12 07:22:56 +00:00
if topic is None:
topic = Topic(name=topic_text)
2010-07-15 20:27:44 +00:00
song.topics.append(topic)
2019-11-09 16:17:41 +00:00
song.temporary = temporary_flag
# We need to save the song now, before adding the media files, so that
# we know where to save the media files to.
2011-03-14 18:59:59 +00:00
clean_song(self.manager, song)
2010-06-28 13:38:29 +00:00
self.manager.save_object(song)
# Now loop through the media files, copy them to the correct location,
# and save the song again.
2017-09-30 20:16:30 +00:00
for file_path, weight in self.media_files:
media_file = self.manager.get_object_filtered(MediaFile, MediaFile.file_path == file_path)
if not media_file:
2017-09-30 20:16:30 +00:00
if file_path.parent:
file_path = self.copy_media_file(song.id, file_path)
song.media_files.append(MediaFile(file_path=file_path, weight=weight))
self.manager.save_object(song)
2014-03-05 18:58:22 +00:00
self.set_defaults()
2019-11-09 16:17:41 +00:00
return song.id
2017-09-30 20:16:30 +00:00
def copy_media_file(self, song_id, file_path):
"""
This method copies the media file to the correct location and returns
the new file location.
2014-03-06 20:40:08 +00:00
:param song_id:
:param pathlib.Path file_path: The file to copy.
2017-09-30 20:16:30 +00:00
:return: The new location of the file
:rtype: pathlib.Path
"""
2013-08-31 18:17:38 +00:00
if not hasattr(self, 'save_path'):
2017-09-30 20:16:30 +00:00
self.save_path = AppLocation.get_section_data_path(self.import_wizard.plugin.name) / 'audio' / str(song_id)
2017-10-10 02:29:56 +00:00
create_paths(self.save_path)
2017-09-30 20:16:30 +00:00
if self.save_path not in file_path.parents:
old_path, file_path = file_path, self.save_path / file_path.name
copyfile(old_path, file_path)
return file_path