songs2slides

A tool that automatically finds song lyrics and creates lyric slideshows
git clone https://git.ashermorgan.net/songs2slides/
Log | Files | Refs | README

commit 070a73a9c03f377a248c60afcd7a8ee7123924f0
parent 4109768149eed0b380a315f62397d162f1680958
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 18 Feb 2024 20:26:41 -0800

Remove v2.2.0 legacy code

Diffstat:
D.github/workflows/main.yml | 25-------------------------
MREADME.md | 26+-------------------------
DSongs2Slides/__init__.py | 7-------
DSongs2Slides/config.py | 46----------------------------------------------
DSongs2Slides/core.py | 224-------------------------------------------------------------------------------
DSongs2Slides/routes.py | 101-------------------------------------------------------------------------------
DSongs2Slides/static/error.js | 8--------
DSongs2Slides/static/favicon-180.png | 0
DSongs2Slides/static/favicon-32.png | 0
DSongs2Slides/static/global.css | 88-------------------------------------------------------------------------------
DSongs2Slides/static/global.js | 32--------------------------------
DSongs2Slides/static/home.css | 110-------------------------------------------------------------------------------
DSongs2Slides/static/home.js | 173-------------------------------------------------------------------------------
DSongs2Slides/static/plus.png | 0
DSongs2Slides/static/settings.css | 32--------------------------------
DSongs2Slides/static/settings.js | 127-------------------------------------------------------------------------------
DSongs2Slides/templates/error.html | 12------------
DSongs2Slides/templates/home.html | 62--------------------------------------------------------------
DSongs2Slides/templates/layout.html | 26--------------------------
DSongs2Slides/templates/settings.html | 130-------------------------------------------------------------------------------
DTests/__init__.py | 0
DTests/test_core.py | 88-------------------------------------------------------------------------------
Dcliapp.py | 106-------------------------------------------------------------------------------
Mrequirements.txt | 6------
Dwebapp.py | 7-------
25 files changed, 1 insertion(+), 1435 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml @@ -1,25 +0,0 @@ -name: Python Application - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Run tests with unittest - run: python -m unittest diff --git a/README.md b/README.md @@ -1,26 +1,2 @@ # Songs2Slides -Creates a lyrics powerpoint from a list of songs. This program does NOT add any copyright information to the powerpoint. The user must do this manually. - -## Features -- Can parse any song given the artist and title. -- New slides are automatically started at the beginning of verses, bridges, choruses, etc. -- The user can easily review and edit the slides. -- The format of the powerpoint can be customized. -- The slides can be added to a new or existing powerpoint. - -## Usage -Note: Songs2Slides requires Python 3.6.x - -Install the python requirements. -``` -pip install -r requirements.txt -``` -To use the command line interface, run `cliapp.py`. -``` -python cliapp.py -``` -To use the web interface, run `webapp.py` and then open http://localhost:5000 in your web browser. -``` -python webapp.py -start http://localhost:5000 -``` +Creates a lyrics powerpoint from a list of songs diff --git a/Songs2Slides/__init__.py b/Songs2Slides/__init__.py @@ -1,6 +0,0 @@ -# Create app -from flask import Flask -app = Flask(__name__) - -# Define routes -from Songs2Slides import routes -\ No newline at end of file diff --git a/Songs2Slides/config.py b/Songs2Slides/config.py @@ -1,46 +0,0 @@ -# Contains default parsing and PowerPoint settings -defaultSettings = { - # Parsing settings - "title-slides": True, - "slide-between-songs": True, - "lines-per-slide": 4, - "remove-parentheses": False, - - # Slide settings - "slide-width": 13.333, - "slide-height": 7.5, - "slide-color": "#000000", - - # Margin settings - "margin-left": 0.5, - "margin-right": 0.5, - "margin-top": 0.5, - "margin-bottom": 0.5, - - # Font settings - "font-family": "Calibri", - "font-size": 40, - "font-bold": False, - "font-italic": False, - "font-color": "#FFFFFF", - - # Paragraph settings - "vertical-alignment": "Middle", # Can be Top, Middle, or Bottom - "line-spacing": 1.25, - "word-wrap": True -} - - - -# Contains cached and custom song information -cachedSongs = { - # Keys should be lowercase ASCII strings without whitespace or special characters - "test-artist-test-song": { - # Title and Artist of the song (formated however you want) - "title":"Test Song", - "artist":"Test Artist", - - # Lyrics with two newlines between each stanza and no newlines at the beginning or end - "lyrics":"test1\ntest2\n\ntest3\ntest4" - } -} diff --git a/Songs2Slides/core.py b/Songs2Slides/core.py @@ -1,224 +0,0 @@ -# Import dependencies -from bs4 import BeautifulSoup -from pptx import Presentation -from pptx.dml.color import RGBColor -from pptx.enum.text import MSO_ANCHOR, PP_ALIGN -from pptx.util import Inches, Pt -import re -import requests -from Songs2Slides import config -from unidecode import unidecode - - - -def GetLyrics(title, artist): - """ - Get the lyrics to a song. - - Parameters - ---------- - title : str - The title of the song. - artist : str - The name of the song's artist. - - Returns - ------- - lyrics : str - The lyrics to the song. - title : str - The title of the song. - artist : str - The name of the song's artist. - """ - - # Convert to lowercase - artist = artist.lower() - title = title.lower() - - # Replace invalid characters - old = [" ", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "'", "?", "/", "|", "\\", ".", ","] - new = ["-", "", "", "", "s", "", "-", "-and-", "", "", "", "-", "-", "", "", "", "", "", "", ""] - for i in range(0, len(old)): - artist = artist.replace(old[i], new[i]) - title = title.replace(old[i], new[i]) - - # Replace unicode characters - artist = unidecode(artist) - title = unidecode(title) - - # Remove unnecessary dashes - artist = "-".join(list(filter(lambda a: a != "", artist.split("-")))) - title = "-".join(list(filter(lambda a: a != "", title.split("-")))) - - # Get song info - if (f"{artist}-{title}" in config.cachedSongs): - # Get the cache key - key = f"{artist}-{title}" - - # Get info from cache - lyrics = config.cachedSongs[key]["lyrics"] - title = config.cachedSongs[key]["title"] - artist = config.cachedSongs[key]["artist"] - else: - # Get page from the internet - page = requests.get(f"https://genius.com/{artist}-{title}-lyrics") - soup = BeautifulSoup(page.text, "html.parser") - - # Find song info - divs = soup.find_all("div", class_="Lyrics__Container-sc-1ynbvzw-8") - lyrics = "\n".join([div.get_text(separator="\n") for div in divs]) - title = soup.find("h1", class_="SongHeader__Title-sc-1b7aqpg-7").get_text() - artist = soup.find("a", class_="SongHeader__Artist-sc-1b7aqpg-9").get_text() - - # Return lyrics - return lyrics, title, artist - - - -def ParseLyrics(title, artist, settings): - """ - Parse the lyrics of a song into slides. - - Parameters - ---------- - title : str - The title of the song. - artist : str - The name of the song's artist. - settings : dict - The settings to use when parsing the lyrics into slides. - - Returns - ------- - list - A list of strings containing the lyrics to the song. Each list item is one slide. - """ - - # Get lyrics - rawLyrics, title, artist = GetLyrics(title, artist) - - # Remove content in parentheses - if (settings["remove-parentheses"]): - rawLyrics = re.sub(r'\s?\([^)]*\)', '', rawLyrics) - - # Parse Lyrics - rawLines = rawLyrics.split("\n") - - # Add title slide - slides = [] - if (settings["title-slides"]): - slides += ["{0}\n{1}".format(title, artist)] - - # Parse lyrics into slides - slideSize = settings["lines-per-slide"] - for i in range(0, len(rawLines)): - if (rawLines[i] == "" or rawLines[i].startswith("[")): - # Start a new slide without content - slides.append("") - slideSize = 0 - elif (slideSize == settings["lines-per-slide"]): - # Start a new slide with content - slides.append(rawLines[i]) - slideSize = 1 - elif (slideSize == 0): - # Continue a blank slide - slides[-1] = slides[-1] + rawLines[i] - slideSize += 1 - else: - # Continue a slide - slides[-1] = slides[-1] + "\n" + rawLines[i] - slideSize += 1 - - # Add/remove blank slide - if (slides[-1] != "" and settings["slide-between-songs"]): - slides += [""] - elif (slides[-1] == "" and not settings["slide-between-songs"]): - del slides[-1] - - # Return parsed lyrics - return slides - - - -def CreatePptx(parsedLyrics, filepath, settings, openFirst): - """ - Create a PowerPoint from a list of lyrics. - - Parameters - ---------- - parsedLyrics : list - The list of strings containing the lyrics. Each list item will be turned into one slide. - filepath : str - The filepath to save the PowerPoint to. - settings : dict - The settings to use while creating the filepath. - openFirst : bool - Whether to add on to the PowerPoint file if it already exists. - """ - - if (openFirst): - try: - # Open presentation - prs = Presentation(filepath) - except: - # Create presentation - prs = Presentation() - - # Set slide width and height - prs.slide_width = Inches(settings["slide-width"]) - prs.slide_height = Inches(settings["slide-height"]) - else: - # Create presentation - prs = Presentation() - - # Set slide width and height - prs.slide_width = Inches(settings["slide-width"]) - prs.slide_height = Inches(settings["slide-height"]) - - # Get blank slide - blank_slide_layout = prs.slide_layouts[6] - - # Get margins - left = Inches(settings["margin-left"]) - top = Inches(settings["margin-top"]) - width = prs.slide_width - Inches(settings["margin-left"] + settings["margin-right"]) - height = prs.slide_height - Inches(settings["margin-top"] + settings["margin-bottom"]) - - for lyric in parsedLyrics: - # Add slide - slide = prs.slides.add_slide(blank_slide_layout) - - # Apply slide formating - slide.background.fill.solid() - slide.background.fill.fore_color.rgb = RGBColor.from_string(settings["slide-color"][1:]) - - # Add text box - txBox = slide.shapes.add_textbox(left, top, width, height) - tf = txBox.text_frame - tf.clear() - - # Apply text formating - tf.word_wrap = settings["word-wrap"] - if (settings["vertical-alignment"].lower() == "top"): - tf.vertical_anchor = MSO_ANCHOR.TOP - elif (settings["vertical-alignment"].lower() == "bottom"): - tf.vertical_anchor = MSO_ANCHOR.BOTTOM - else: - tf.vertical_anchor = MSO_ANCHOR.MIDDLE - - # Add pharagraph - p = tf.paragraphs[0] - p.text = lyric - - # Apply pharagraph formating - p.font.name = settings["font-family"] - p.font.size = Pt(settings["font-size"]) - p.font.bold = settings["font-bold"] - p.font.italic = settings["font-italic"] - p.font.color.rgb = RGBColor.from_string(settings["font-color"][1:]) - p.alignment = PP_ALIGN.CENTER - p.line_spacing = settings["line-spacing"] - - # Save powerpoint - prs.save(filepath) diff --git a/Songs2Slides/routes.py b/Songs2Slides/routes.py @@ -1,101 +0,0 @@ -# Import dependencies -from copy import deepcopy -from flask import render_template, request, send_file, url_for, jsonify -import io -import json -import os -from Songs2Slides import app, core -from Songs2Slides.config import defaultSettings -import tempfile - - - -# Home page -@app.route("/", methods=["GET"]) -def home(): - return render_template("home.html") - - - -# Settings page -@app.route("/settings/", methods=["GET"]) -def settings(): - return render_template("settings.html", title = "Settings") - - - -# Settings JSON file -@app.route("/settings.json", methods=["GET"]) -def settingsJSON(): - return jsonify(defaultSettings) - - - -# Get Powerpoint -@app.route("/pptx", methods=["POST"]) -def pptx(): - # Get settings - settings = deepcopy(defaultSettings) - try: - for setting in request.json["settings"]: - settings[setting] = request.json["settings"][setting] - except: - pass - - try: - # Get temp - temp = tempfile.NamedTemporaryFile(mode="w+t", suffix=".pptx", delete=False) - temp.close() - - # Get lyrics - lyrics = json.loads(request.form["lyrics"]) - - # Save uploaded powerpoint - if (request.files["pptxFile"].filename != ""): - request.files["pptxFile"].save(temp.name) - - # Create powerpoint - core.CreatePptx(lyrics, temp.name, settings, True) - - # Read file into stream - with open(temp.name, 'rb') as f: - pptx = io.BytesIO(f.read()) - finally: - # Delete temp file - os.remove(temp.name) - - # Return powerpoint - return send_file(pptx, as_attachment=True, attachment_filename='download.pptx') - - - -# Get lyrics -@app.route("/lyrics", methods=["POST"]) -def lyrics(): - # Get settings - settings = deepcopy(defaultSettings) - try: - for setting in request.json["settings"]: - settings[setting] = request.json["settings"][setting] - except: - pass - - # Get lyrics - lyrics = [] - failed = [] - for song in request.json["songs"]: - try: - lyrics += core.ParseLyrics(song[0], song[1], settings) - except: - failed += [song] - - # Return lyrics - return jsonify({"lyrics": lyrics, "errors": failed}) - - - -# 404 page -@app.errorhandler(404) -def error404(e): - message = "The requested URL was not found on the server." - return render_template("error.html", title="404 Not Found", code="404", message=message), 404 diff --git a/Songs2Slides/static/error.js b/Songs2Slides/static/error.js @@ -1,8 +0,0 @@ -/** - * Finishes setting up the page - * @returns {void} - */ -function onLoad() { - // Set theme - UpdateTheme(); -} diff --git a/Songs2Slides/static/favicon-180.png b/Songs2Slides/static/favicon-180.png Binary files differ. diff --git a/Songs2Slides/static/favicon-32.png b/Songs2Slides/static/favicon-32.png Binary files differ. diff --git a/Songs2Slides/static/global.css b/Songs2Slides/static/global.css @@ -1,88 +0,0 @@ -/******** Variable styles ********/ -:root { - --theme-color: #ffff50; - --foreground-color: #000000; - --background-color: #ffffff; - --border-color: #808080; - --hover-color: #f0f0f0; -} - - - -/******** Global styles ********/ -body { - text-align: center; - font-family: Arial, Helvetica, sans-serif; - touch-action: manipulation; - margin-top: 80px; - color: var(--foreground-color); - background-color: var(--background-color); -} - -button:not(.songRemove), input, select, textarea { - background-color: var(--background-color); - border: 1px solid var(--border-color); - color: var(--foreground-color); - border-radius: 0px; -} - -button:hover:enabled:not(.songRemove) { - cursor: pointer; - background-color: var(--hover-color); -} - -button:disabled { - background-color: var(--hover-color); - color: var(--border-color); -} - -input[type="file"] { - background-color: var(--hover-color); - border: none; -} - -h1 { - margin-top: 15px; - margin-bottom: 15px; -} - -#header { - position: fixed; - top: 0px; - left: 0px; - width: 100%; - background-color: var(--theme-color); -} - - -.container { - margin: auto; - padding: 10px; - background-color: var(--hover-color); -} - -button { - height: 25px; -} - - - -/******** Dark styles ********/ -body.dark { - --foreground-color: #e0e0e0; - --background-color: #121416; - --hover-color: #323436; - --border-color: #505050; -} - -.dark h1 { - color: #000000; -} - -.dark button:not(.songRemove), .dark input, .dark select { - --background-color: #222426; -} - -.dark a { - color: #0080ff; -} diff --git a/Songs2Slides/static/global.js b/Songs2Slides/static/global.js @@ -1,32 +0,0 @@ -/** - * Updates the interface theme - * @param {bool} theme - False for light mode, true for dark mode - * @returns {void} - */ -function UpdateTheme(theme = null) { - // Get theme from localStorage - if (theme === null) { - theme = localStorage.getItem("theme"); - } - - // Detect preferred color scheme - if (theme === null) { - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - theme = "Dark"; - } - else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { - theme = "Light"; - } - } - - // Apply theme - if (theme === "Dark") { - document.body.classList.add("dark"); - } - else { - document.body.classList.remove("dark"); - } - - // Save theme - localStorage.setItem("theme", theme); -} diff --git a/Songs2Slides/static/home.css b/Songs2Slides/static/home.css @@ -1,110 +0,0 @@ -/******** Variable styles ********/ -:root { - --song-color: #C0C0C0; - --error-color: #E00000; -} - - - -/******** Songs styles ********/ -#songsContainer { - width: fit-content; - width: -moz-fit-content; -} - -h3 { - margin-top: 0px; -} - -.song { - width: fit-content; - width: -moz-fit-content; - margin-left: auto; - margin-right: auto; - margin-top: 10px; - margin-bottom: 10px; - padding: 8px; - background: var(--song-color); - border-radius: 5px; -} - -.songRemove { - border: none; - padding: 0px; - margin: 0px; - cursor: pointer; - color: var(--foreground-color); - background-color: var(--song-color); -} - -#addSong { - display: inline-block; - padding: 0px; - height: 40px; - width: 40px; - background-color: var(--song-color); - border: none; - border-radius: 50%; -} - -#addSong img { - height: 40px; - width: 40px; -} - -#songsButtons { - margin-top: 15px; - height: 25px; - text-align: left; -} - -#reviewButton { - float: right; -} - - - -/******** Lyrics styles ********/ -#lyricsContainer { - max-width: 500px; -} - -#lyricsContainer p { - text-align: left; -} - -#rawLyrics { - width: 100%; - resize: none; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -#fileUpload { - text-align: left; - margin-top: 5px; -} - -#errors { - margin-top: 15px; - color: var(--error-color); - font-weight: bold; -} - -#lyricsButtons { - margin-top: 15px; - text-align: left; - height: 25px; -} - -#submitLyricsButton { - float: right; -} - - - -/******** Dark styles ********/ -body.dark { - --song-color: #525456 -} diff --git a/Songs2Slides/static/home.js b/Songs2Slides/static/home.js @@ -1,173 +0,0 @@ -// Declare global variables -setId = 0; // Next valid song id number - - - -/** - * Finishes setting up the page - * @returns {void} - */ -function onLoad() { - // Add song - AddSong(); - - // Set theme - UpdateTheme(); -} - - - -/** - * Adds a blank song to the songs div - * @returns {void} - */ -function AddSong() { - // Create row - let clone = document.getElementById("songTemplate").content.cloneNode(true); - - // Set row id - clone.children[0].setAttribute("id", `song-${setId}`); - - // Add remove button onclick event - clone.getElementById("remove").setAttribute("onclick", `let element = document.getElementById('song-${setId}'); element.parentNode.removeChild(element);`); - - // Add row - document.getElementById("songs").appendChild(clone); - - // Increment setId - setId++; -} - - - -/** - * Gets the list of songs in the songs div - * @returns {list} - List of songs information - */ -function getSongs() { - // Get song info - let titles = []; - for (title of document.getElementsByClassName("title")) { - titles.push(title.value); - } - let artists = []; - for (artist of document.getElementsByClassName("artist")) { - artists.push(artist.value); - } - - // Prepare songs - let songs = [] - for (let i = 0; i < titles.length; i++) { - songs.push([titles[i], artists[i]]) - } - - // Set songs - return songs -} - - - -/** - * Gets the parsed lyrics for the user to review - * @returns {void} - */ -async function ReviewLyrics() { - // Show and hide elements - document.getElementById("rawLyrics").value = "Loading lyrics..."; - document.getElementById("rawLyrics").readOnly = true; - document.getElementById("errors").textContent = ""; - document.getElementById("songsContainer").hidden = true; - document.getElementById("lyricsContainer").hidden = false; - - // Get songs - let songs = getSongs(); - - // Send POST request - const rawResponse = await fetch("/lyrics", { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, body: JSON.stringify({"songs":songs,"settings":JSON.parse(localStorage.getItem("settings"))}) - }); - const json = await rawResponse.json(); - - // Set lyrics - document.getElementById("rawLyrics").value = json["lyrics"].join("\n\n") - document.getElementById("rawLyrics").readOnly = false; - - // Set errors - if (json["errors"].length != 0) - { - let errors = "The lyrics to the following songs could not be found: "; - for (error of json["errors"]) { - errors += `"${error[0]}" by "${error[1]}", `; - } - errors = errors.slice(0, -2); // Remove trailing ', ' - document.getElementById("errors").textContent = errors; - } -} - - - -/** - * Prepares the lyrics form to be submitted and shows the thankyou screen - * @returns {void} - */ -async function SubmitLyrics() { - // Get lyrics - let lyrics = document.getElementById("rawLyrics").value.split(/\n\s*\n/); - - // Set hidden form values - document.getElementById("pptxSettingsField").value = localStorage.getItem("settings"); - document.getElementById("lyricsField").value = JSON.stringify(lyrics); - - // Show and hide elements - document.getElementById("lyricsContainer").hidden = true; - document.getElementById("thankyou").hidden = false; -} - - - -/** - * Makes the previous screen visible - * @returns {void} - */ -function Back() { - if (document.getElementById("lyricsContainer").hidden == false) { - document.getElementById("songsContainer").hidden = false; - document.getElementById("lyricsContainer").hidden = true; - document.getElementById("thankyou").hidden = true; - } - else if (document.getElementById("thankyou").hidden == false) { - document.getElementById("songsContainer").hidden = true; - document.getElementById("lyricsContainer").hidden = false; - document.getElementById("thankyou").hidden = true; - } -} - - - -/** - * Clears the list of songs and makes the songs screen visible - * @returns {void} - */ -function Reset() { - // Remove songs - let songs = document.getElementsByClassName("song"); - while (songs.length > 0) { - // Get song - songs[0].parentNode.removeChild(songs[0]); - } - - // Reset pptx file input - document.getElementsByName("pptxFile")[0].value = null; - - // Add blank song - AddSong(); - - // Makes songs visible - document.getElementById("songsContainer").hidden = false; - document.getElementById("lyricsContainer").hidden = true; - document.getElementById("thankyou").hidden = true; -} diff --git a/Songs2Slides/static/plus.png b/Songs2Slides/static/plus.png Binary files differ. diff --git a/Songs2Slides/static/settings.css b/Songs2Slides/static/settings.css @@ -1,32 +0,0 @@ -/******** Settings styles ********/ -#settings { - max-width: 300px; - text-align: left; - line-height: 30px; -} - -h2 { - margin-top: 0px; - margin-bottom: 0px; - text-align: center; -} - -h4:first-of-type { - margin-top: 0px; -} - -h4 { - margin-bottom: 0px; -} - -input[type="number"] { - width: 75px; -} - -#settingsFooter { - margin-top: 15px; -} - -#resetButton { - float: right; -} diff --git a/Songs2Slides/static/settings.js b/Songs2Slides/static/settings.js @@ -1,127 +0,0 @@ -/** - * Finishes setting up the page - * @returns {void} - */ -function onLoad() { - // Load settings - if (localStorage.getItem("settings") == null) { - resetSettings(); - } - else { - loadSettings(JSON.parse(localStorage.getItem("settings"))); - } -} - - - -// Loads settings -/** - * Loads settings - * @param {object} settings - The settings object - * @returns {void} - */ -function loadSettings(settings) { - // Interface settings (not stored with other settings) - UpdateTheme(); - if (document.body.classList.contains("dark")) { - document.getElementById("theme").value = "Dark"; - } - else { - document.getElementById("theme").value = "Light"; - } - - // Parsing settings - document.getElementById("title-slides").checked = settings["title-slides"]; - document.getElementById("slide-between-songs").checked = settings["slide-between-songs"]; - document.getElementById("lines-per-slide").value = settings["lines-per-slide"]; - document.getElementById("remove-parentheses").checked = settings["remove-parentheses"]; - - // Slide settings - document.getElementById("slide-width").value = settings["slide-width"]; - document.getElementById("slide-height").value = settings["slide-height"]; - document.getElementById("slide-color").value = settings["slide-color"]; - - // Margin settings - document.getElementById("margin-left").value = settings["margin-left"]; - document.getElementById("margin-right").value = settings["margin-right"]; - document.getElementById("margin-top").value = settings["margin-top"]; - document.getElementById("margin-bottom").value = settings["margin-bottom"]; - - // Font settings - document.getElementById("font-family").value = settings["font-family"]; - document.getElementById("font-size").value = settings["font-size"]; - document.getElementById("font-bold").checked = settings["font-bold"]; - document.getElementById("font-italic").checked = settings["font-italic"]; - document.getElementById("font-color").value = settings["font-color"]; - - // Pharagraph settings - document.getElementById("vertical-alignment").value = settings["vertical-alignment"]; - document.getElementById("line-spacing").value = settings["line-spacing"]; - document.getElementById("word-wrap").checked = settings["word-wrap"]; -} - - - -/** - * Saves settings to localStorage - * @returns {void} - */ -function saveSettings() { - // Save interface settings and update interface - UpdateTheme(document.getElementById("theme").value); - - // Get settings - const settings = { - // Parsing settings - "title-slides": document.getElementById("title-slides").checked, - "slide-between-songs": document.getElementById("slide-between-songs").checked, - "lines-per-slide": Number(document.getElementById("lines-per-slide").value), - "remove-parentheses": document.getElementById("remove-parentheses").checked, - - // Slide settings - "slide-width": Number(document.getElementById("slide-width").value), - "slide-height": Number(document.getElementById("slide-height").value), - "slide-color": document.getElementById("slide-color").value, - - // Margin settings - "margin-left": Number(document.getElementById("margin-left").value), - "margin-right": Number(document.getElementById("margin-right").value), - "margin-top": Number(document.getElementById("margin-top").value), - "margin-bottom": Number(document.getElementById("margin-bottom").value), - - // Font settings - "font-family": document.getElementById("font-family").value, - "font-size": Number(document.getElementById("font-size").value), - "font-bold": document.getElementById("font-bold").checked, - "font-italic": document.getElementById("font-italic").checked, - "font-color": document.getElementById("font-color").value, - - // Pharagraph settings - "vertical-alignment": document.getElementById("vertical-alignment").value, - "line-spacing": Number(document.getElementById("line-spacing").value), - "word-wrap": document.getElementById("word-wrap").checked, - } - - // Save settings - localStorage.setItem("settings", JSON.stringify(settings)); -} - - - -/** - * Resets all settings to their default values - * @returns {void} - */ -async function resetSettings() { - // Send POST request - const rawResponse = await fetch("/settings.json", { - method: 'GET' - }); - const json = await rawResponse.json(); - - // Load changes - loadSettings(json); - - // Save changes - localStorage.setItem("settings", JSON.stringify(json)); -} diff --git a/Songs2Slides/templates/error.html b/Songs2Slides/templates/error.html @@ -1,12 +0,0 @@ -{% extends "layout.html" %} - -{% block header %} - <script src="{{ url_for('static', filename='error.js') }}"></script> -{% endblock header %} - -{% block content %} - <h1>{{ code }}</h1> - <p>{{ message }}</p> - - <p><a href="{{ url_for('home') }}">Return to the homepage</a></p> -{% endblock content %} diff --git a/Songs2Slides/templates/home.html b/Songs2Slides/templates/home.html @@ -1,62 +0,0 @@ -{% extends "layout.html" %} - -{% block header %} - <link rel="stylesheet" href="{{ url_for('static', filename='home.css') }}"></link> - <script src="{{ url_for('static', filename='home.js') }}"></script> -{% endblock header %} - -{% block content %} - <div id="songsContainer" class="container"> - <h3>Choose your songs</h3> - - <template id="songTemplate"> - <div class="song"> - <input type="text" class="title" placeholder="Song Title"/> - <input type="text" class="artist" placeholder="Song Artist"/> - <button id="remove" class="songRemove">╳</button> - </div> - </template> - <div id="songs"></div> - - <div> - <button id="addSong" onclick="AddSong();"> - <img src="{{ url_for('static', filename='plus.png')}}"/> - </button> - </div> - - <div id="songsButtons"> - <a href="/settings/">Settings</a> - <button id="reviewButton" onclick="ReviewLyrics();">Next</button> - </div> - </div> - - <form id="lyricsContainer" class="container" hidden action="/pptx" method="POST" enctype="multipart/form-data"> - <h3>Review your PowerPoint</h3> - - <p>Review and edit the parsed lyrics below and then click the create PowerPoint button.</p> - <p>One blank line represents the start of a new slide and three blank lines represent a blank slide.</p> - - <input type="text" id="pptxSettingsField" name="settings" hidden> - <input type="text" id="lyricsField" name="lyrics" hidden> - - <textarea rows="15" id="rawLyrics"></textarea> - - <div id="fileUpload"> - <label>Starting PowerPoint (optional): </label> - <input type="file" name="pptxFile" accept="application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.ms-powerpoint"> - </div> - - <p id="errors"></p> - - <div id="lyricsButtons"> - <a id="lyricsBack" href="javascript:Back()">Back</a> - <button id="submitLyricsButton" onclick="SubmitLyrics()">Create Powerpoint</button> - </div> - </form> - - <div id="thankyou" hidden> - <h3>Thank you for using Songs2Slides</h3> - <p><a href="javascript:Back()">Edit your PowerPoint</a></p> - <p><a href="javascript:Reset()">Create another PowerPoint</a></p> - </div> -{% endblock content %} diff --git a/Songs2Slides/templates/layout.html b/Songs2Slides/templates/layout.html @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - {% if title %} - <title>{{ title }}</title> - {% else %} - <title>SongsSlides</title> - {% endif %} - - <meta name="description" content="Quickly and easily create lyric powerpoints from a list of songs."> - <meta name="viewport" content="width=device-width, user-scalable=no"/> - <link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon-32.png') }}"> - <link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='favicon-180.png') }}"> - <link rel="stylesheet" href="{{ url_for('static', filename='global.css') }}"></link> - <script src="{{ url_for('static', filename='global.js') }}"></script> - - {% block header %}{% endblock %} - </head> - <body onload="onLoad();"> - <div id="header"> - <h1>Songs2Slides</h1> - </div> - - {% block content %}{% endblock %} - </body> -</html> diff --git a/Songs2Slides/templates/settings.html b/Songs2Slides/templates/settings.html @@ -1,130 +0,0 @@ -{% extends "layout.html" %} - -{% block header %} - <link rel="stylesheet" href="{{ url_for('static', filename='settings.css') }}"></link> - <script src="{{ url_for('static', filename='settings.js') }}"></script> -{% endblock header %} - -{% block content %} - <div id="settings" class="container"> - <h2>Settings</h2> - <h4>Interface</h4> - <div> - Theme: - <select id="theme" onchange="saveSettings();"> - <option>Light</option> - <option>Dark</option> - </select> - </div> - <h4>Parsing</h4> - <div> - Add title slides: - <input type="checkbox" id="title-slides" onchange="saveSettings();"/> - </div> - <div> - Blank slide between songs: - <input type="checkbox" id="slide-between-songs" onchange="saveSettings();"/> - </div> - <div> - Default lines per slide: - <input type="number" min="1" step="1" id="lines-per-slide" onchange="saveSettings();"/> - </div> - <div> - Remove content in parentheses: - <input type="checkbox" id="remove-parentheses" onchange="saveSettings();"/> - </div> - - <h4>Slide settings</h4> - <div> - Width (Inches): - <input type="number" min="0" step="any" id="slide-width" onchange="saveSettings();"/> - </div> - <div> - Height (Inches): - <input type="number" min="0" step="any" id="slide-height" onchange="saveSettings();"/> - </div> - <div> - Background color: - <input type="color" id="slide-color" onchange="saveSettings();"/> - </div> - - <h4>Margin settings</h4> - <div> - Left (Inches): - <input type="number" min="0" step="any" id="margin-left" onchange="saveSettings();"/> - </div> - <div> - Right (Inches): - <input type="number" min="0" step="any" id="margin-right" onchange="saveSettings();"/> - </div> - <div> - Top (Inches): - <input type="number" min="0" step="any" id="margin-top" onchange="saveSettings();"/> - </div> - <div> - Bottom (Inches): - <input type="number" min="0" step="any" id="margin-bottom" onchange="saveSettings();"/> - </div> - - <h4>Font settings</h4> - <div> - Family: - <input id="font-family" list="fonts" onchange="saveSettings();"> - <datalist id="fonts"> - <option>Arial</option> - <option>Century</option> - <option>Century Gothic</option> - <option>Calibri</option> - <option>Comic Sans MS</option> - <option>Courier New</option> - <option>Georgia</option> - <option>Impact</option> - <option>Rockwell</option> - <option>Segoe UI</option> - <option>Tahoma</option> - <option>Times New Roman</option> - <option>Trebuchet MS</option> - <option>Verdana</option> - </datalist> - </div> - <div> - Size: - <input type="number" min="0" step="1" id="font-size" onchange="saveSettings();"/> - </div> - <div> - Bold: - <input type="checkbox" id = "font-bold" onchange="saveSettings();"/> - </div> - <div> - Italic: - <input type="checkbox" id="font-italic" onchange="saveSettings();"/> - </div> - <div> - Color: - <input type="color" id="font-color" onchange="saveSettings();"/> - </div> - - <h4>Pharagraph settings</h4> - <div> - Vertical Alignment: - <select id="vertical-alignment" onchange="saveSettings();"> - <option>Top</option> - <option>Middle</option> - <option>Bottom</option> - </select> - </div> - <div> - Line spacing: - <input type="number" min="0" step="any" id="line-spacing" onchange="saveSettings();"/> - </div> - <div> - Word wrap: - <input type="checkbox" id="word-wrap" onchange="saveSettings();"/> - </div> - - <div id="settingsFooter"> - <a href="../">Back</a> - <button id="resetButton" onclick="resetSettings();">Reset</button> - </div> - </div> -{% endblock content %} diff --git a/Tests/__init__.py b/Tests/__init__.py diff --git a/Tests/test_core.py b/Tests/test_core.py @@ -1,88 +0,0 @@ -# Import dependencies -from Songs2Slides import core, config -import unittest -from unittest.mock import patch - - - -class TestCore(unittest.TestCase): - # Test GetLyrics method on cached songs - def test_GetLyrics_cache(self): - # Set cached songs - config.cachedSongs = { - "test-artist-test-song": { - "title": "Test Song", - "artist": "Test Artist", - "lyrics":"test1\ntest2\n\ntest3\ntest4" - } - } - - # Test cached song - lyrics, title, artist = core.GetLyrics("tEsT sOnG", "tEsT aRtIsT") - self.assertEqual(lyrics, "test1\ntest2\n\ntest3\ntest4") - self.assertEqual(title, "Test Song") - self.assertEqual(artist, "Test Artist") - - - - # Test GetLyrics method on songs on the internet - def test_GetLyrics_web(self): - with patch('Songs2Slides.core.requests.get') as mocked_get: - # Initialize mocked_get - mocked_get.return_value.text = b"<!DOCTYPE html><html><head></head><body><h1 class=\"SongHeader__Title-sc-1b7aqpg-7\">Test Song 2</h1><a class=\"SongHeader__Artist-sc-1b7aqpg-9\">Test Artist</a><div class=\"Lyrics__Root-sc-1ynbvzw-1\"><div class=\"Lyrics__Container-sc-1ynbvzw-8\">[Verse 1]<br><a><span>Test1<br>Test2<br>Test3</span></a><br><a><span>Test4<br>Test5</span></a></div><div class=\"Lyrics__Container-sc-1ynbvzw-8\">[Verse 2]<br><a><span>Test10<br>Test20</span></a><br>Test30<br>Test40<br><a><span>Test50</span></a></div></div></body></html>" - - # Get song lyrics - lyrics, title, artist = core.GetLyrics("tEsT sOnG 2", "tEsT aRtIsT") - - # Validate responce - mocked_get.assert_called_with("https://genius.com/test-artist-test-song-2-lyrics") - self.assertEqual(lyrics, "[Verse 1]\nTest1\nTest2\nTest3\nTest4\nTest5\n[Verse 2]\nTest10\nTest20\nTest30\nTest40\nTest50") - self.assertEqual(title, "Test Song 2") - self.assertEqual(artist, "Test Artist") - - - - # Test ParseLyrics method - def test_ParseLyrics(self): - # Initialize settings - settings = { - "title-slides": True, - "slide-between-songs": True, - "lines-per-slide": 4, - "remove-parentheses": False, - } - - # Mock core.getLyrics method - with patch('Songs2Slides.core.GetLyrics') as mocked_get: - # Initialize mocked_get - mocked_get.return_value = ("[Verse 1]\nTest1\nTest2\nTest3\nTest4\nTest5 (Test5)\n[Verse 2]\nTest10\nTest20\nTest30\nTest40\nTest50(Test50)", "Test Song", "Test Artist") - - # Test parser - lyrics = core.ParseLyrics("tEsT sOnG 2", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test Song\nTest Artist", "Test1\nTest2\nTest3\nTest4", "Test5 (Test5)", "Test10\nTest20\nTest30\nTest40", "Test50(Test50)", ""]) - mocked_get.assert_called_with("tEsT sOnG 2", "tEsT aRtIsT") - - # Test parser without title slide - settings["title-slides"] = False - lyrics = core.ParseLyrics("tEsT sOnG", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test1\nTest2\nTest3\nTest4", "Test5 (Test5)", "Test10\nTest20\nTest30\nTest40", "Test50(Test50)", ""]) - - # Test parser without slide at end - settings["slide-between-songs"] = False - lyrics = core.ParseLyrics("tEsT sOnG", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test1\nTest2\nTest3\nTest4", "Test5 (Test5)", "Test10\nTest20\nTest30\nTest40", "Test50(Test50)"]) - - # Test parser with 3 lines per slide - settings["lines-per-slide"] = 3 - lyrics = core.ParseLyrics("tEsT sOnG", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test1\nTest2\nTest3", "Test4\nTest5 (Test5)", "Test10\nTest20\nTest30", "Test40\nTest50(Test50)"]) - - # Test parser with parentheses remover - settings["remove-parentheses"] = True - lyrics = core.ParseLyrics("tEsT sOnG 2", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test1\nTest2\nTest3", "Test4\nTest5", "Test10\nTest20\nTest30", "Test40\nTest50"]) - - # Test parser with blank line - mocked_get.return_value = ("[Verse 1]\nTest1\n[Instrumental]\n[Verse 2]\nTest2", "Test Song", "Test Artist") - lyrics = core.ParseLyrics("tEsT sOnG 2", "tEsT aRtIsT", settings) - self.assertEqual(lyrics, ["Test1", "", "Test2"]) diff --git a/cliapp.py b/cliapp.py @@ -1,106 +0,0 @@ -# Import dependencies -import os -import re -from Songs2Slides import core -from Songs2Slides.config import defaultSettings -import subprocess -import sys -import tempfile - - - -# Run CLI -if (__name__ == "__main__"): - # Print title - print("Songs2Slides") - print() - - # Get song lyrics - lyrics = [] - song = 1 - while (True): - # Get song information - title = input("Song #{0} Title: ".format(song)) - artist = input("Song #{0} Artist: ".format(song)) - - # Get song lyrics - try: - lyrics += core.ParseLyrics(title, artist, defaultSettings) - except: - print("The song could not be found. Make sure that you spelled it correctly.") - song -= 1 - - # Add more songs - if (song >= 1 and input("Do you want to add another song? (y/n): ").lower() == "n"): - break - else: - song += 1 - - # Review lyrics - if (input("Do you want to review the parsed lyrics first? (y/n): ").lower() == "y"): - try: - # Create temp file - temp = tempfile.NamedTemporaryFile(mode="w+t", suffix=".txt", delete=False) - temp.writelines("\n\n".join(lyrics)) - temp.close() - - # Open temp file and wait - if (sys.platform == "win32"): - subprocess.Popen(["notepad", temp.name]).wait() - elif (sys.platform == "darwin"): - subprocess.Popen(["open", temp.name]).wait() - else: - subprocess.Popen(["xdg-open", temp.name]).wait() - - # Read file - with open(temp.name) as f: - rawLines = f.read() - - # Parse lyrics - lyrics = re.split("\n\s*\n", rawLines) - except: - print("There was an error while reviewing the lyrics. The unrevised lyrics will be used instead.") - finally: - # Delete temp file - os.remove(temp.name) - - # Get filepath - filepath = input("Enter a filepath to save the powerpoint to: ") - - # Add extension - if (len(filepath) == 0): - filepath = "Untitled.pptx" - elif (len(filepath) < 4): - filepath += ".pptx" - elif (len(filepath) == 4 and filepath[-4:] != ".ppt"): - filepath += ".pptx" - elif (len(filepath) > 4 and filepath[-5:] != ".pptx" and filepath[-4:] != ".ppt"): - filepath += ".pptx" - - # Confirm overwrite - if (os.path.exists(filepath)): - openFirst = (input("This powerpoint already exists. Do you want to add on to it? (y/n): ").lower() == "y") - else: - openFirst = False - - # Create powerpoint - try: - core.CreatePptx(lyrics, filepath, defaultSettings, openFirst) - except: - print("There was an error while creating the powerpoint.") - input("Press enter to exit...") - sys.exit() - - # Open powerpoint - if (input("Do you want to view the powerpoint now? (y/n): ").lower() == "y"): - try: - if (sys.platform == "win32"): - os.startfile(filepath) - elif (sys.platform == "darwin"): - subprocess.Popen(["open", filepath]) - else: - subprocess.Popen(["xdg-open", filepath]) - except: - print("There was an error while opening the powerpoint.") - input("Press enter to exit...") - sys.exit() diff --git a/requirements.txt b/requirements.txt @@ -1,5 +0,0 @@ -bs4 -python-pptx -requests -flask -unidecode -\ No newline at end of file diff --git a/webapp.py b/webapp.py @@ -1,6 +0,0 @@ -# Import app -from Songs2Slides import app - -# Run app -if (__name__ == "__main__"): - app.run(debug=True) -\ No newline at end of file