openlp/openlp/plugins/songs/lib/songselect.py

277 lines
11 KiB
Python
Raw Normal View History

# -*- 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/>. #
##########################################################################
"""
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
"""
import logging
import re
2017-12-28 08:08:12 +00:00
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
2014-07-03 16:54:51 +00:00
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.core.common.utils import wait_for
2018-10-02 04:39:42 +00:00
BASE_URL = 'https://songselect.ccli.com'
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
'https%3a%2f%2fsongselect.ccli.com%2f'
2017-03-10 05:42:38 +00:00
LOGIN_URL = 'https://profile.ccli.com'
LOGOUT_URL = BASE_URL + '/account/logout'
SEARCH_URL = BASE_URL + '/search/results'
2019-09-26 20:49:05 +00:00
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
2019-03-05 21:21:12 +00:00
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):
2014-05-02 06:42:17 +00:00
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]
2016-08-13 14:41:24 +00:00
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']):
2015-11-22 14:06:54 +00:00
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)
2014-05-07 10:25:36 +00:00
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)
2014-05-07 10:21:27 +00:00
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)
2014-03-06 21:28:13 +00:00
return db_song