mirror of https://gitlab.com/openlp/openlp.git
277 lines
11 KiB
Python
277 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2023 OpenLP Developers #
|
|
# ---------------------------------------------------------------------- #
|
|
# 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/>. #
|
|
##########################################################################
|
|
"""
|
|
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
|
|
"""
|
|
import logging
|
|
import re
|
|
from html import unescape
|
|
from urllib.request import URLError
|
|
from PyQt5 import QtCore
|
|
from bs4 import BeautifulSoup, NavigableString
|
|
|
|
from openlp.plugins.songs.lib import VerseType, clean_song
|
|
from openlp.plugins.songs.lib.db import Song, Author, Topic
|
|
from openlp.plugins.songs.lib.openlyricsxml import SongXML
|
|
from openlp.core.common.utils import wait_for
|
|
|
|
|
|
BASE_URL = 'https://songselect.ccli.com'
|
|
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
|
|
'https%3a%2f%2fsongselect.ccli.com%2f'
|
|
LOGIN_URL = 'https://profile.ccli.com'
|
|
LOGOUT_URL = BASE_URL + '/account/logout'
|
|
SEARCH_URL = BASE_URL + '/search/results'
|
|
SONG_PAGE = BASE_URL + '/Songs/'
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Pages(object):
|
|
"""
|
|
Songselect web page types.
|
|
"""
|
|
Login = 0
|
|
Home = 1
|
|
Search = 2
|
|
Song = 3
|
|
Other = 4
|
|
|
|
|
|
class SongSelectImport(object):
|
|
"""
|
|
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
|
|
with CCLI's SongSelect service and downloads the songs.
|
|
"""
|
|
def __init__(self, db_manager, webview):
|
|
"""
|
|
Set up the song select importer
|
|
|
|
:param db_manager: The song database manager
|
|
"""
|
|
self.db_manager = db_manager
|
|
self.webview = webview
|
|
|
|
def get_page_type(self):
|
|
"""
|
|
Get the type of page the user is currently ono
|
|
|
|
:return: The page the user is on
|
|
"""
|
|
current_url_host = self.webview.page().url().host()
|
|
current_url_path = self.webview.page().url().path()
|
|
if (current_url_host == QtCore.QUrl(LOGIN_URL).host() and current_url_path == QtCore.QUrl(LOGIN_PAGE).path()):
|
|
return Pages.Login
|
|
elif (current_url_host == QtCore.QUrl(BASE_URL).host()):
|
|
if (current_url_path == '/' or current_url_path == ''):
|
|
return Pages.Home
|
|
elif (current_url_path == QtCore.QUrl(SEARCH_URL).path()):
|
|
return Pages.Search
|
|
elif (self.get_song_number_from_url(current_url_path) is not None):
|
|
return Pages.Song
|
|
return Pages.Other
|
|
|
|
def _run_javascript(self, script):
|
|
"""
|
|
Run a script and returns the result
|
|
|
|
:param script: The javascript to be run
|
|
:return: The evaluated result
|
|
"""
|
|
self.web_stuff = ""
|
|
self.got_web_stuff = False
|
|
|
|
def handle_result(result):
|
|
"""
|
|
Handle the result from the asynchronous call
|
|
"""
|
|
self.got_web_stuff = True
|
|
self.web_stuff = result
|
|
self.webview.page().runJavaScript(script, handle_result)
|
|
wait_for(lambda: self.got_web_stuff)
|
|
return self.web_stuff
|
|
|
|
def reset_webview(self):
|
|
"""
|
|
Sets the webview back to the login page using the Qt setUrl method
|
|
"""
|
|
url = QtCore.QUrl(LOGIN_PAGE)
|
|
self.webview.setUrl(url)
|
|
|
|
def set_home_page(self):
|
|
"""
|
|
Sets the webview to the search page
|
|
"""
|
|
self.set_page(BASE_URL)
|
|
|
|
def set_page(self, url):
|
|
"""
|
|
Sets the active page in the webview
|
|
|
|
:param url: The new page location
|
|
"""
|
|
script = 'document.location = "{}"'.format(url)
|
|
self._run_javascript(script)
|
|
|
|
def set_login_fields(self, username, password):
|
|
script_set_login_fields = ('document.getElementById("EmailAddress").value = "{email}";'
|
|
'document.getElementById("Password").value = "{password}";'
|
|
).format(email=username, password=password)
|
|
self._run_javascript(script_set_login_fields)
|
|
|
|
def get_page(self, url):
|
|
"""
|
|
Gets the html for the url through the active webview
|
|
|
|
:return: String containing a html document
|
|
"""
|
|
script_get_page = ('var openlp_page_data = null;'
|
|
'fetch("{}").then(data => {{return data.text()}})'
|
|
' .then(data => {{openlp_page_data = data}})').format(url)
|
|
self._run_javascript(script_get_page)
|
|
wait_for(lambda: self._run_javascript('openlp_page_data != null'))
|
|
return self._run_javascript('openlp_page_data')
|
|
|
|
def get_song_number_from_url(self, url):
|
|
"""
|
|
Gets the ccli song number for a song from the url
|
|
|
|
:return: String containg ccli song number, None is returned if not found
|
|
"""
|
|
ccli_number_regex = re.compile(r'.*?Songs\/([0-9]+).*', re.IGNORECASE)
|
|
regex_matches = ccli_number_regex.match(url)
|
|
if regex_matches:
|
|
return regex_matches.group(1)
|
|
return None
|
|
|
|
def get_song(self, callback=None):
|
|
"""
|
|
Get the full song from SongSelect
|
|
|
|
:param song: The song page url
|
|
:param callback: A callback which can be used to indicate progress
|
|
:return: Dictionary containing the song info
|
|
"""
|
|
song = {}
|
|
# Get current song
|
|
current_url = self.webview.url().path()
|
|
ccli_number = self.get_song_number_from_url(current_url)
|
|
|
|
if callback:
|
|
callback()
|
|
try:
|
|
song_page = BeautifulSoup(self.get_page(SONG_PAGE + ccli_number), 'lxml')
|
|
except (TypeError, URLError) as error:
|
|
log.exception('Could not get song from SongSelect, {error}'.format(error=error))
|
|
return None
|
|
try:
|
|
lyrics_link = song_page.find('section', 'page-section').find('a')['href']
|
|
except KeyError:
|
|
# can't find a link to the song - most likely the user account has no access to it
|
|
return None
|
|
if callback:
|
|
callback()
|
|
try:
|
|
lyrics_page = BeautifulSoup(self.get_page(BASE_URL + lyrics_link), 'lxml')
|
|
except (TypeError, URLError):
|
|
log.exception('Could not get lyrics from SongSelect')
|
|
return None
|
|
if callback:
|
|
callback()
|
|
theme_elements = []
|
|
# Themes regex only works if the ccli site is in english.
|
|
themes_regex = re.compile(r'\bThemes\b')
|
|
for ul in song_page.find_all('ul', 'song-meta-list'):
|
|
if ul.find('li', string=themes_regex):
|
|
theme_elements.extend(ul.find_all('li')[1:])
|
|
copyright_elements = lyrics_page.find('ul', 'copyright').find_all('li')
|
|
author_elements = song_page.find('div', 'content-title').find('ul', 'authors').find_all('li')
|
|
song['title'] = unescape(song_page.find('div', 'content-title').find('h1').string.strip())
|
|
song['authors'] = [unescape(li.find('a').string).strip() for li in author_elements]
|
|
song['copyright'] = '/'.join([unescape(li.string).strip() for li in copyright_elements])
|
|
song['topics'] = [unescape(li.string).strip() for li in theme_elements]
|
|
song['ccli_number'] = song_page.find('div', 'song-content-data').find('ul').find('li')\
|
|
.find('strong').string.strip()
|
|
song['verses'] = []
|
|
verses = lyrics_page.find('div', 'song-viewer lyrics').find_all('p')
|
|
verse_labels = lyrics_page.find('div', 'song-viewer lyrics').find_all('h3')
|
|
for verse, label in zip(verses, verse_labels):
|
|
song_verse = {'label': unescape(label.string).strip(), 'lyrics': ''}
|
|
for v in verse.contents:
|
|
if isinstance(v, NavigableString):
|
|
song_verse['lyrics'] += unescape(v.string).strip()
|
|
else:
|
|
song_verse['lyrics'] += '\n'
|
|
song_verse['lyrics'] = song_verse['lyrics'].strip(' \n\r\t')
|
|
song['verses'].append(song_verse)
|
|
for counter, author in enumerate(song['authors']):
|
|
song['authors'][counter] = unescape(author)
|
|
return song
|
|
|
|
def save_song(self, song):
|
|
"""
|
|
Save a song to the database, using the db_manager
|
|
|
|
:param song: Dictionary of the song to save
|
|
:return:
|
|
"""
|
|
db_song = Song(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
|
|
song_xml = SongXML()
|
|
verse_order = []
|
|
for verse in song['verses']:
|
|
if ' ' in verse['label']:
|
|
verse_type, verse_number = verse['label'].split(' ', 1)
|
|
else:
|
|
verse_type = verse['label']
|
|
verse_number = 1
|
|
verse_type = VerseType.from_loose_input(verse_type)
|
|
try:
|
|
verse_number = int(verse_number)
|
|
except ValueError:
|
|
# Some custom verse types contain multiple words, and this messes with the verse number,
|
|
# so just default to 1 on ValueError. See https://gitlab.com/openlp/openlp/-/issues/937
|
|
verse_number = 1
|
|
song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
|
|
verse_order.append('{tag}{number}'.format(tag=VerseType.tags[verse_type], number=verse_number))
|
|
db_song.verse_order = ' '.join(verse_order)
|
|
db_song.lyrics = song_xml.extract_xml()
|
|
clean_song(self.db_manager, db_song)
|
|
self.db_manager.save_object(db_song)
|
|
db_song.authors_songs = []
|
|
for author_name in song['authors']:
|
|
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
|
|
if not author:
|
|
name_parts = author_name.rsplit(' ', 1)
|
|
first_name = name_parts[0]
|
|
if len(name_parts) == 1:
|
|
last_name = ''
|
|
else:
|
|
last_name = name_parts[1]
|
|
author = Author(first_name=first_name, last_name=last_name, display_name=author_name)
|
|
db_song.add_author(author)
|
|
for topic_name in song.get('topics', []):
|
|
topic = self.db_manager.get_object_filtered(Topic, Topic.name == topic_name)
|
|
if not topic:
|
|
topic = Topic(name=topic_name)
|
|
db_song.topics.append(topic)
|
|
self.db_manager.save_object(db_song)
|
|
return db_song
|