forked from openlp/openlp
r1341
This commit is contained in:
commit
0cd10c83e1
@ -91,6 +91,7 @@ class BaseModel(object):
|
|||||||
instance.__setattr__(key, kwargs[key])
|
instance.__setattr__(key, kwargs[key])
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
class Manager(object):
|
||||||
"""
|
"""
|
||||||
Provide generic object persistence management
|
Provide generic object persistence management
|
||||||
|
@ -34,6 +34,15 @@ from openlp.core.ui import MainDisplay
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VERSE = u'The Lord said to {r}Noah{/r}: \n' \
|
||||||
|
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
|
||||||
|
'The Lord said to {g}Noah{/g}:\n' \
|
||||||
|
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
|
||||||
|
'Get those children out of the muddy, muddy \n' \
|
||||||
|
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
|
||||||
|
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||||
|
FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
|
||||||
|
|
||||||
class RenderManager(object):
|
class RenderManager(object):
|
||||||
"""
|
"""
|
||||||
Class to pull all Renderer interactions into one place. The plugins will
|
Class to pull all Renderer interactions into one place. The plugins will
|
||||||
@ -202,28 +211,17 @@ class RenderManager(object):
|
|||||||
self.force_page = force_page
|
self.force_page = force_page
|
||||||
# set the default image size for previews
|
# set the default image size for previews
|
||||||
self.calculate_default(self.screens.preview[u'size'])
|
self.calculate_default(self.screens.preview[u'size'])
|
||||||
verse = u'The Lord said to {r}Noah{/r}: \n' \
|
|
||||||
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
|
|
||||||
'The Lord said to {g}Noah{/g}:\n' \
|
|
||||||
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
|
|
||||||
'Get those children out of the muddy, muddy \n' \
|
|
||||||
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
|
|
||||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
|
||||||
# make big page for theme edit dialog to get line count
|
|
||||||
if self.force_page:
|
|
||||||
verse = verse + verse + verse
|
|
||||||
else:
|
|
||||||
self.image_manager.del_image(theme_data.theme_name)
|
|
||||||
footer = []
|
|
||||||
footer.append(u'Arky Arky (Unknown)')
|
|
||||||
footer.append(u'Public Domain')
|
|
||||||
footer.append(u'CCLI 123456')
|
|
||||||
# build a service item to generate preview
|
# build a service item to generate preview
|
||||||
serviceItem = ServiceItem()
|
serviceItem = ServiceItem()
|
||||||
serviceItem.theme = theme_data
|
serviceItem.theme = theme_data
|
||||||
serviceItem.add_from_text(u'', verse, footer)
|
if self.force_page:
|
||||||
|
# make big page for theme edit dialog to get line count
|
||||||
|
serviceItem.add_from_text(u'', VERSE + VERSE + VERSE, FOOTER)
|
||||||
|
else:
|
||||||
|
self.image_manager.del_image(theme_data.theme_name)
|
||||||
|
serviceItem.add_from_text(u'', VERSE, FOOTER)
|
||||||
serviceItem.render_manager = self
|
serviceItem.render_manager = self
|
||||||
serviceItem.raw_footer = footer
|
serviceItem.raw_footer = FOOTER
|
||||||
serviceItem.render(True)
|
serviceItem.render(True)
|
||||||
if not self.force_page:
|
if not self.force_page:
|
||||||
self.display.buildHtml(serviceItem)
|
self.display.buildHtml(serviceItem)
|
||||||
|
@ -91,6 +91,7 @@ class ThemeLevel(object):
|
|||||||
Service = 2
|
Service = 2
|
||||||
Song = 3
|
Song = 3
|
||||||
|
|
||||||
|
|
||||||
class BackgroundType(object):
|
class BackgroundType(object):
|
||||||
"""
|
"""
|
||||||
Type enumeration for backgrounds.
|
Type enumeration for backgrounds.
|
||||||
@ -123,6 +124,7 @@ class BackgroundType(object):
|
|||||||
elif type_string == u'image':
|
elif type_string == u'image':
|
||||||
return BackgroundType.Image
|
return BackgroundType.Image
|
||||||
|
|
||||||
|
|
||||||
class BackgroundGradientType(object):
|
class BackgroundGradientType(object):
|
||||||
"""
|
"""
|
||||||
Type enumeration for background gradients.
|
Type enumeration for background gradients.
|
||||||
@ -200,6 +202,7 @@ INTEGER_LIST = [u'size', u'line_adjustment', u'x', u'height', u'y',
|
|||||||
u'width', u'shadow_size', u'outline_size', u'horizontal_align',
|
u'width', u'shadow_size', u'outline_size', u'horizontal_align',
|
||||||
u'vertical_align', u'wrap_style']
|
u'vertical_align', u'wrap_style']
|
||||||
|
|
||||||
|
|
||||||
class ThemeXML(object):
|
class ThemeXML(object):
|
||||||
"""
|
"""
|
||||||
A class to encapsulate the Theme XML.
|
A class to encapsulate the Theme XML.
|
||||||
|
@ -279,7 +279,7 @@ class Ui_MainWindow(object):
|
|||||||
add_actions(self.SettingsLanguageMenu, self.LanguageGroup.actions())
|
add_actions(self.SettingsLanguageMenu, self.LanguageGroup.actions())
|
||||||
add_actions(self.SettingsMenu, (self.settingsPluginListItem,
|
add_actions(self.SettingsMenu, (self.settingsPluginListItem,
|
||||||
self.SettingsLanguageMenu.menuAction(), None,
|
self.SettingsLanguageMenu.menuAction(), None,
|
||||||
self.SettingsShortcutsItem, self.DisplayTagItem,
|
self.DisplayTagItem, self.SettingsShortcutsItem,
|
||||||
self.SettingsConfigureItem))
|
self.SettingsConfigureItem))
|
||||||
add_actions(self.ToolsMenu, (self.ToolsAddToolItem, None))
|
add_actions(self.ToolsMenu, (self.ToolsAddToolItem, None))
|
||||||
add_actions(self.ToolsMenu, (self.ToolsOpenDataFolder, None))
|
add_actions(self.ToolsMenu, (self.ToolsOpenDataFolder, None))
|
||||||
|
@ -35,7 +35,7 @@ import socket
|
|||||||
import urllib
|
import urllib
|
||||||
from HTMLParser import HTMLParseError
|
from HTMLParser import HTMLParseError
|
||||||
|
|
||||||
from BeautifulSoup import BeautifulSoup, NavigableString
|
from BeautifulSoup import BeautifulSoup, NavigableString, Tag
|
||||||
|
|
||||||
from openlp.core.lib import Receiver, translate
|
from openlp.core.lib import Receiver, translate
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
@ -221,21 +221,14 @@ class BGExtract(object):
|
|||||||
crossrefs = soup.findAll(u'sup', u'xref')
|
crossrefs = soup.findAll(u'sup', u'xref')
|
||||||
if crossrefs:
|
if crossrefs:
|
||||||
[crossref.extract() for crossref in crossrefs]
|
[crossref.extract() for crossref in crossrefs]
|
||||||
|
headings = soup.findAll(u'h5')
|
||||||
|
if headings:
|
||||||
|
[heading.extract() for heading in headings]
|
||||||
cleanup = [(re.compile('\s+'), lambda match: ' ')]
|
cleanup = [(re.compile('\s+'), lambda match: ' ')]
|
||||||
verses = BeautifulSoup(str(soup), markupMassage=cleanup)
|
verses = BeautifulSoup(str(soup), markupMassage=cleanup)
|
||||||
content = verses.find(u'div', u'result-text-style-normal')
|
|
||||||
if not content:
|
|
||||||
content = verses.find(u'div', u'result-text-style-rtl-serif')
|
|
||||||
if not content:
|
|
||||||
log.debug(u'No content found in the BibleGateway response.')
|
|
||||||
send_error_message(u'parse')
|
|
||||||
return None
|
|
||||||
verse_count = len(verses.findAll(u'sup', u'versenum'))
|
|
||||||
found_count = 0
|
|
||||||
verse_list = {}
|
verse_list = {}
|
||||||
while found_count < verse_count:
|
for verse in verses(u'sup', u'versenum'):
|
||||||
content = content.findNext(u'sup', u'versenum')
|
raw_verse_num = verse.next
|
||||||
raw_verse_num = content.next
|
|
||||||
clean_verse_num = 0
|
clean_verse_num = 0
|
||||||
# Not all verses exist in all translations and may or may not be
|
# Not all verses exist in all translations and may or may not be
|
||||||
# represented by a verse number. If they are not fine, if they are
|
# represented by a verse number. If they are not fine, if they are
|
||||||
@ -248,9 +241,22 @@ class BGExtract(object):
|
|||||||
log.exception(u'Illegal verse number in %s %s %s:%s',
|
log.exception(u'Illegal verse number in %s %s %s:%s',
|
||||||
version, bookname, chapter, unicode(raw_verse_num))
|
version, bookname, chapter, unicode(raw_verse_num))
|
||||||
if clean_verse_num:
|
if clean_verse_num:
|
||||||
raw_verse_text = raw_verse_num.next
|
verse_text = raw_verse_num.next
|
||||||
verse_list[clean_verse_num] = unicode(raw_verse_text)
|
part = raw_verse_num.next.next
|
||||||
found_count += 1
|
while not (isinstance(part, Tag) and part.attrMap and
|
||||||
|
part.attrMap[u'class'] == u'versenum'):
|
||||||
|
# While we are still in the same verse grab all the text.
|
||||||
|
if isinstance(part, NavigableString):
|
||||||
|
verse_text = verse_text + part
|
||||||
|
if isinstance(part.next, Tag) and part.next.name == u'div':
|
||||||
|
# Run out of verses so stop.
|
||||||
|
break
|
||||||
|
part = part.next
|
||||||
|
verse_list[clean_verse_num] = unicode(verse_text)
|
||||||
|
if not verse_list:
|
||||||
|
log.debug(u'No content found in the BibleGateway response.')
|
||||||
|
send_error_message(u'parse')
|
||||||
|
return None
|
||||||
return SearchResults(bookname, chapter, verse_list)
|
return SearchResults(bookname, chapter, verse_list)
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,16 +46,6 @@ except ImportError:
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BibleMode(object):
|
|
||||||
"""
|
|
||||||
This is basically an enumeration class which specifies the mode of a Bible.
|
|
||||||
Mode refers to whether or not a Bible in OpenLP is a full Bible or needs to
|
|
||||||
be downloaded from the Internet on an as-needed basis.
|
|
||||||
"""
|
|
||||||
Full = 1
|
|
||||||
Partial = 2
|
|
||||||
|
|
||||||
|
|
||||||
class BibleFormat(object):
|
class BibleFormat(object):
|
||||||
"""
|
"""
|
||||||
This is a special enumeration class that holds the various types of Bibles,
|
This is a special enumeration class that holds the various types of Bibles,
|
||||||
|
@ -482,7 +482,7 @@ class BibleMediaItem(MediaManagerItem):
|
|||||||
self.listView.clear()
|
self.listView.clear()
|
||||||
if self.listView.count() != 0:
|
if self.listView.count() != 0:
|
||||||
self.__checkSecondBible(bible, second_bible)
|
self.__checkSecondBible(bible, second_bible)
|
||||||
else:
|
elif self.search_results:
|
||||||
self.displayResults(bible, second_bible)
|
self.displayResults(bible, second_bible)
|
||||||
Receiver.send_message(u'cursor_normal')
|
Receiver.send_message(u'cursor_normal')
|
||||||
self.advancedSearchButton.setEnabled(True)
|
self.advancedSearchButton.setEnabled(True)
|
||||||
@ -698,11 +698,7 @@ class BibleMediaItem(MediaManagerItem):
|
|||||||
service_item.add_capability(ItemCapabilities.AllowsPreview)
|
service_item.add_capability(ItemCapabilities.AllowsPreview)
|
||||||
service_item.add_capability(ItemCapabilities.AllowsLoop)
|
service_item.add_capability(ItemCapabilities.AllowsLoop)
|
||||||
# Service Item: Title
|
# Service Item: Title
|
||||||
for title in raw_title:
|
service_item.title = u', '.join(raw_title)
|
||||||
if not service_item.title:
|
|
||||||
service_item.title = title
|
|
||||||
else:
|
|
||||||
service_item.title += u', ' + title
|
|
||||||
# Service Item: Theme
|
# Service Item: Theme
|
||||||
if len(self.settings.bible_theme) == 0:
|
if len(self.settings.bible_theme) == 0:
|
||||||
service_item.theme = None
|
service_item.theme = None
|
||||||
|
@ -213,6 +213,7 @@ class Controller(object):
|
|||||||
def poll(self):
|
def poll(self):
|
||||||
self.doc.poll_slidenumber(self.is_live)
|
self.doc.poll_slidenumber(self.is_live)
|
||||||
|
|
||||||
|
|
||||||
class MessageListener(object):
|
class MessageListener(object):
|
||||||
"""
|
"""
|
||||||
This is the Presentation listener who acts on events from the slide
|
This is the Presentation listener who acts on events from the slide
|
||||||
|
@ -129,6 +129,7 @@ class FieldDescEntry:
|
|||||||
self.type = type
|
self.type = type
|
||||||
self.size = size
|
self.size = size
|
||||||
|
|
||||||
|
|
||||||
class EasyWorshipSongImport(SongImport):
|
class EasyWorshipSongImport(SongImport):
|
||||||
"""
|
"""
|
||||||
The :class:`EasyWorshipSongImport` class provides OpenLP with the
|
The :class:`EasyWorshipSongImport` class provides OpenLP with the
|
||||||
|
@ -133,6 +133,7 @@ class FoilPresenterImport(SongImport):
|
|||||||
log.exception(u'XML syntax error in file %s' % file_path)
|
log.exception(u'XML syntax error in file %s' % file_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FoilPresenter(object):
|
class FoilPresenter(object):
|
||||||
"""
|
"""
|
||||||
This class represents the converter for Foilpresenter XML from a song.
|
This class represents the converter for Foilpresenter XML from a song.
|
||||||
@ -259,7 +260,6 @@ class FoilPresenter(object):
|
|||||||
copyright = None
|
copyright = None
|
||||||
if copyright:
|
if copyright:
|
||||||
strings = []
|
strings = []
|
||||||
author_temp = []
|
|
||||||
if copyright.find(u'Copyright') != -1:
|
if copyright.find(u'Copyright') != -1:
|
||||||
temp = copyright.partition(u'Copyright')
|
temp = copyright.partition(u'Copyright')
|
||||||
copyright = temp[0]
|
copyright = temp[0]
|
||||||
@ -305,7 +305,7 @@ class FoilPresenter(object):
|
|||||||
while i != 1:
|
while i != 1:
|
||||||
if copyright.find(u'<marker>') != -1:
|
if copyright.find(u'<marker>') != -1:
|
||||||
temp = copyright.partition(u'<marker>')
|
temp = copyright.partition(u'<marker>')
|
||||||
if (temp[0].strip() != u'') & (x > 0):
|
if temp[0].strip() and x > 0:
|
||||||
strings.append(temp[0])
|
strings.append(temp[0])
|
||||||
copyright = temp[2]
|
copyright = temp[2]
|
||||||
x += 1
|
x += 1
|
||||||
@ -314,14 +314,15 @@ class FoilPresenter(object):
|
|||||||
i = 1
|
i = 1
|
||||||
else:
|
else:
|
||||||
i = 1
|
i = 1
|
||||||
|
author_temp = []
|
||||||
for author in strings:
|
for author in strings:
|
||||||
temp = re.split(u',(?=\D{2})|(?<=\D),|\/(?=\D{3,})|(?<=\D);',
|
temp = re.split(u',(?=\D{2})|(?<=\D),|\/(?=\D{3,})|(?<=\D);',
|
||||||
author)
|
author)
|
||||||
for tempx in temp:
|
for tempx in temp:
|
||||||
author_temp.append(tempx)
|
author_temp.append(tempx)
|
||||||
for author in author_temp:
|
for author in author_temp:
|
||||||
regex = u'^[\/,;\-\s]+|[\/,;\-\s]+$|'\
|
regex = u'^[\/,;\-\s\.]+|[\/,;\-\s\.]+$|'\
|
||||||
'\s*[0-9]{4}\s*[\-\/]?\s*([0-9]{4})?[\/,;\-\s]*$'
|
'\s*[0-9]{4}\s*[\-\/]?\s*([0-9]{4})?[\/,;\-\s\.]*$'
|
||||||
author = re.compile(regex).sub(u'', author)
|
author = re.compile(regex).sub(u'', author)
|
||||||
author = re.compile(
|
author = re.compile(
|
||||||
u'[0-9]{1,2}\.\s?J(ahr)?h\.|um\s*$|vor\s*$').sub(u'',
|
u'[0-9]{1,2}\.\s?J(ahr)?h\.|um\s*$|vor\s*$').sub(u'',
|
||||||
@ -330,12 +331,12 @@ class FoilPresenter(object):
|
|||||||
author = author.strip()
|
author = author.strip()
|
||||||
if re.search(
|
if re.search(
|
||||||
u'\w+\.?\s+\w{3,}\s+[a|u]nd\s|\w+\.?\s+\w{3,}\s+&\s',
|
u'\w+\.?\s+\w{3,}\s+[a|u]nd\s|\w+\.?\s+\w{3,}\s+&\s',
|
||||||
author, re.U) != None:
|
author, re.U):
|
||||||
temp = re.split(u'\s[a|u]nd\s|\s&\s', author)
|
temp = re.split(u'\s[a|u]nd\s|\s&\s', author)
|
||||||
for tempx in temp:
|
for tempx in temp:
|
||||||
tempx = tempx.strip()
|
tempx = tempx.strip()
|
||||||
authors.append(tempx)
|
authors.append(tempx)
|
||||||
elif (len(author) > 2):
|
elif len(author) > 2:
|
||||||
authors.append(author)
|
authors.append(author)
|
||||||
for display_name in authors:
|
for display_name in authors:
|
||||||
author = self.manager.get_object_filtered(Author,
|
author = self.manager.get_object_filtered(Author,
|
||||||
|
@ -53,6 +53,7 @@ class SongSearch(object):
|
|||||||
Authors = 4
|
Authors = 4
|
||||||
Themes = 5
|
Themes = 5
|
||||||
|
|
||||||
|
|
||||||
class SongMediaItem(MediaManagerItem):
|
class SongMediaItem(MediaManagerItem):
|
||||||
"""
|
"""
|
||||||
This is the custom media manager item for Songs.
|
This is the custom media manager item for Songs.
|
||||||
@ -218,13 +219,9 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
self.listView.clear()
|
self.listView.clear()
|
||||||
searchresults.sort(cmp=self.collateSongTitles)
|
searchresults.sort(cmp=self.collateSongTitles)
|
||||||
for song in searchresults:
|
for song in searchresults:
|
||||||
author_list = u''
|
author_list = [author.display_name for author in song.authors]
|
||||||
for author in song.authors:
|
|
||||||
if author_list != u'':
|
|
||||||
author_list = author_list + u', '
|
|
||||||
author_list = author_list + author.display_name
|
|
||||||
song_title = unicode(song.title)
|
song_title = unicode(song.title)
|
||||||
song_detail = u'%s (%s)' % (song_title, author_list)
|
song_detail = u'%s (%s)' % (song_title, u', '.join(author_list))
|
||||||
song_name = QtGui.QListWidgetItem(song_detail)
|
song_name = QtGui.QListWidgetItem(song_detail)
|
||||||
song_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(song.id))
|
song_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(song.id))
|
||||||
self.listView.addItem(song_name)
|
self.listView.addItem(song_name)
|
||||||
@ -334,9 +331,6 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
def generateSlideData(self, service_item, item=None, xmlVersion=False):
|
def generateSlideData(self, service_item, item=None, xmlVersion=False):
|
||||||
log.debug(u'generateSlideData (%s:%s)' % (service_item, item))
|
log.debug(u'generateSlideData (%s:%s)' % (service_item, item))
|
||||||
raw_footer = []
|
raw_footer = []
|
||||||
author_list = u''
|
|
||||||
author_audit = []
|
|
||||||
ccli = u''
|
|
||||||
item_id = self._getIdOfItemToGenerate(item, self.remoteSong)
|
item_id = self._getIdOfItemToGenerate(item, self.remoteSong)
|
||||||
service_item.add_capability(ItemCapabilities.AllowsEdit)
|
service_item.add_capability(ItemCapabilities.AllowsEdit)
|
||||||
service_item.add_capability(ItemCapabilities.AllowsPreview)
|
service_item.add_capability(ItemCapabilities.AllowsPreview)
|
||||||
@ -397,13 +391,9 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
for slide in verses:
|
for slide in verses:
|
||||||
service_item.add_from_text(slide[:30], unicode(slide))
|
service_item.add_from_text(slide[:30], unicode(slide))
|
||||||
service_item.title = song.title
|
service_item.title = song.title
|
||||||
for author in song.authors:
|
author_list = [unicode(author.display_name) for author in song.authors]
|
||||||
if len(author_list) > 1:
|
|
||||||
author_list = author_list + u', '
|
|
||||||
author_list = author_list + unicode(author.display_name)
|
|
||||||
author_audit.append(unicode(author.display_name))
|
|
||||||
raw_footer.append(song.title)
|
raw_footer.append(song.title)
|
||||||
raw_footer.append(author_list)
|
raw_footer.append(u', '.join(author_list))
|
||||||
raw_footer.append(song.copyright)
|
raw_footer.append(song.copyright)
|
||||||
if QtCore.QSettings().value(u'general/ccli number',
|
if QtCore.QSettings().value(u'general/ccli number',
|
||||||
QtCore.QVariant(u'')).toString():
|
QtCore.QVariant(u'')).toString():
|
||||||
@ -413,10 +403,10 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
QtCore.QVariant(u'')).toString()))
|
QtCore.QVariant(u'')).toString()))
|
||||||
service_item.raw_footer = raw_footer
|
service_item.raw_footer = raw_footer
|
||||||
service_item.audit = [
|
service_item.audit = [
|
||||||
song.title, author_audit, song.copyright, unicode(song.ccli_number)
|
song.title, author_list, song.copyright, unicode(song.ccli_number)
|
||||||
]
|
]
|
||||||
service_item.data_string = {u'title': song.search_title,
|
service_item.data_string = {u'title': song.search_title,
|
||||||
u'authors': author_list}
|
u'authors': u', '.join(author_list)}
|
||||||
service_item.xml_version = self.openLyrics.song_to_xml(song)
|
service_item.xml_version = self.openLyrics.song_to_xml(song)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ from openlp.plugins.songs.lib.songimport import SongImport
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
#TODO: Use lxml for parsing and make sure we use methods of "SongImport" .
|
|
||||||
class OpenSongImport(SongImport):
|
class OpenSongImport(SongImport):
|
||||||
"""
|
"""
|
||||||
Import songs exported from OpenSong
|
Import songs exported from OpenSong
|
||||||
|
Loading…
Reference in New Issue
Block a user