songs2slides

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

commit 7d76c2d1987db4118760a39c6e076a2b01a29115
parent 5f362372df19b2dd58ef2732b78e6e8ed67dfd82
Author: AsherMorgan <59518073+AsherMorgan@users.noreply.github.com>
Date:   Thu, 23 Apr 2020 19:31:25 -0700

Merge pull request #1 from AsherMorgan/feature-web

Implement web interface.
Diffstat:
DSongs2Slides.py | 253-------------------------------------------------------------------------------
ASongs2Slides/__init__.py | 7+++++++
ASongs2Slides/config.py | 32++++++++++++++++++++++++++++++++
ASongs2Slides/models.py | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ASongs2Slides/routes.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
ASongs2Slides/static/index.css | 19+++++++++++++++++++
ASongs2Slides/static/index.js | 47+++++++++++++++++++++++++++++++++++++++++++++++
ASongs2Slides/templates/index.html | 34++++++++++++++++++++++++++++++++++
Acliapp.py | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrequirements.txt | 5+++--
Dsettings.json | 24------------------------
Awebapp.py | 7+++++++
12 files changed, 448 insertions(+), 279 deletions(-)

diff --git a/Songs2Slides.py b/Songs2Slides.py @@ -1,252 +0,0 @@ -# Import dependencies -from bs4 import BeautifulSoup -import json -import os -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 requests -import subprocess -import sys -import tempfile - - - -# Gets the lyrics -def GetLyrics(artist, song): - # Convert to lowercase - artist = artist.lower() - song = song.lower() - - # Replace invalid characters - old = [" ", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "'", "?", "/", "|", "\\", ".", ",", "á", "é", "í", "ó", "ñ", "ú"] - new = ["-", "", "", "", "s", "", "-", "-and-", "", "", "", "-", "-", "", "", "", "", "", "", "", "a", "e", "i", "o", "n", "u"] - for i in range(0, len(old)): - artist = artist.replace(old[i], new[i]) - song = song.replace(old[i], new[i]) - - # Remove unnecessary dashes - artist = "-".join(list(filter(lambda a: a != "", artist.split("-")))) - song = "-".join(list(filter(lambda a: a != "", song.split("-")))) - - # Get lyrics - page = requests.get("https://genius.com/{0}-{1}-lyrics".format(artist, song)) - lyrics = BeautifulSoup(page.text, "html.parser").find("div", class_="lyrics").get_text() - - # Return lyrics - return lyrics - - - -# Parses the lyrics into blocks -def ParseLyrics(lyrics, settings): - # Split lyrics - rawLines = lyrics.split("\n") - - # Remove starting and ending newlines - del rawLines[0] - del rawLines[0] - del rawLines[-1] - del rawLines[-1] - - # Parse lyrics into lines - lines = [] - slideSize = settings["lines-per-slide"] - for i in range(0, len(rawLines)): - if (rawLines[i] == ""): - # Start a new slide without content - lines.append("") - slideSize = 0 - elif (rawLines[i][0] == "["): - # Ignore - pass - elif (slideSize == settings["lines-per-slide"]): - # Start a new slide with content - lines.append(rawLines[i]) - slideSize = 1 - elif (slideSize == 0): - # Continue a blank slide - lines[-1] = lines[-1] + rawLines[i] - slideSize += 1 - else: - # Continue a slide - lines[-1] = lines[-1] + "\n" + rawLines[i] - slideSize += 1 - return lines - - - -# Create powerpoint -def CreatePptx(parsedLyrics, filepath, openFirst, settings): - 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(settings["slide-color"][0], settings["slide-color"][1], settings["slide-color"][2]) - - # 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(settings["font-color"][0], settings["font-color"][1], settings["font-color"][2]) - p.alignment = PP_ALIGN.CENTER - p.line_spacing = settings["line-spacing"] - - # Save powerpoint - prs.save(filepath) - - - -# Run CLI -if (__name__ == "__main__"): - # Load settings - try: - with open(os.path.join(os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))), "settings.json")) as f: - settings = json.load(f) - except: - print("There was an error while loading the settings.") - input("Press enter to exit...") - sys.exit() - - # 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: - parsedLyrics = ParseLyrics(GetLyrics(artist, title), settings) - if (settings["title-slides"]): - lyrics += ["{0}\n{1}".format(title, artist)] - lyrics += parsedLyrics - if (lyrics[-1] != ""): - lyrics += [""] - 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) - for line in lyrics: - temp.writelines(line) - temp.writelines("\n\n") - temp.close() - - # Open temp file and wait - subprocess.Popen(["notepad", temp.name]).wait() - - # Read file - with open(temp.name) as f: - rawLines = f.read() - - # Parse lyrics - newLyrics = rawLines.split("\n\n") - del newLyrics[-1] - lyrics = newLyrics - 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: - CreatePptx(lyrics, filepath, openFirst, settings) - 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: - os.startfile(filepath) - except: - print("There was an error while opening the powerpoint.") - input("Press enter to exit...") - sys.exit() -\ No newline at end of file diff --git a/Songs2Slides/__init__.py b/Songs2Slides/__init__.py @@ -0,0 +1,6 @@ +# 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 @@ -0,0 +1,31 @@ +parsing = { + "title-slides": True, + "lines-per-slide": 4, +} + +slide = { + "width": 13.333, + "height": 7.5, + "color": [0, 0, 0], +} + +margin = { + "left": 0.5, + "right": 0.5, + "top": 0.5, + "bottom": 0.5, +} + +font = { + "family": "Calibri", + "size": 40, + "bold": False, + "italic": False, + "color": [255, 255, 255], +} + +paragraph = { + "vertical-alignment": "middle", # Can be top, middle, or bottom + "line-spacing": 1.25, + "word-wrap": True +} +\ No newline at end of file diff --git a/Songs2Slides/models.py b/Songs2Slides/models.py @@ -0,0 +1,142 @@ +# Import dependencies +from bs4 import BeautifulSoup +from Songs2Slides import config +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 requests + + + +# Gets the lyrics +def GetLyrics(artist, song): + # Convert to lowercase + artist = artist.lower() + song = song.lower() + + # Replace invalid characters + old = [" ", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "'", "?", "/", "|", "\\", ".", ",", "á", "é", "í", "ó", "ñ", "ú"] + new = ["-", "", "", "", "s", "", "-", "-and-", "", "", "", "-", "-", "", "", "", "", "", "", "", "a", "e", "i", "o", "n", "u"] + for i in range(0, len(old)): + artist = artist.replace(old[i], new[i]) + song = song.replace(old[i], new[i]) + + # Remove unnecessary dashes + artist = "-".join(list(filter(lambda a: a != "", artist.split("-")))) + song = "-".join(list(filter(lambda a: a != "", song.split("-")))) + + # Get lyrics + page = requests.get("https://genius.com/{0}-{1}-lyrics".format(artist, song)) + lyrics = BeautifulSoup(page.text, "html.parser").find("div", class_="lyrics").get_text() + + # Return lyrics + return lyrics + + + +# Parses the lyrics into blocks +def ParseLyrics(lyrics): + # Split lyrics + rawLines = lyrics.split("\n") + + # Remove starting and ending newlines + del rawLines[0] + del rawLines[0] + del rawLines[-1] + del rawLines[-1] + + # Parse lyrics into lines + lines = [] + slideSize = config.parsing["lines-per-slide"] + for i in range(0, len(rawLines)): + if (rawLines[i] == ""): + # Start a new slide without content + lines.append("") + slideSize = 0 + elif (rawLines[i][0] == "["): + # Ignore + pass + elif (slideSize == config.parsing["lines-per-slide"]): + # Start a new slide with content + lines.append(rawLines[i]) + slideSize = 1 + elif (slideSize == 0): + # Continue a blank slide + lines[-1] = lines[-1] + rawLines[i] + slideSize += 1 + else: + # Continue a slide + lines[-1] = lines[-1] + "\n" + rawLines[i] + slideSize += 1 + return lines + + + +# Create powerpoint +def CreatePptx(parsedLyrics, filepath, openFirst): + if (openFirst): + try: + # Open presentation + prs = Presentation(filepath) + except: + # Create presentation + prs = Presentation() + + # Set slide width and height + prs.slide_width = Inches(config.slide["width"]) + prs.slide_height = Inches(config.slide["height"]) + else: + # Create presentation + prs = Presentation() + + # Set slide width and height + prs.slide_width = Inches(config.slide["width"]) + prs.slide_height = Inches(config.slide["height"]) + + # Get blank slide + blank_slide_layout = prs.slide_layouts[6] + + # Get margins + left = Inches(config.margin["left"]) + top = Inches(config.margin["top"]) + width = prs.slide_width - Inches(config.margin["left"] + config.margin["right"]) + height = prs.slide_height - Inches(config.margin["top"] + config.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(config.slide["color"][0], config.slide["color"][1], config.slide["color"][2]) + + # Add text box + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.clear() + + # Apply text formating + tf.word_wrap = config.paragraph["word-wrap"] + if (config.paragraph["vertical-alignment"].lower() == "top"): + tf.vertical_anchor = MSO_ANCHOR.TOP + elif (config.paragraph["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 = config.font["family"] + p.font.size = Pt(config.font["size"]) + p.font.bold = config.font["bold"] + p.font.italic = config.font["italic"] + p.font.color.rgb = RGBColor(config.font["color"][0], config.font["color"][1], config.font["color"][2]) + p.alignment = PP_ALIGN.CENTER + p.line_spacing = config.paragraph["line-spacing"] + + # Save powerpoint + prs.save(filepath) +\ No newline at end of file diff --git a/Songs2Slides/routes.py b/Songs2Slides/routes.py @@ -0,0 +1,51 @@ +# Import dependencies +from flask import render_template, request, send_file, url_for +import io +import json +import os +from Songs2Slides import app, models, config +import tempfile + + + +# Home page +@app.route("/", methods=["GET"]) +def index(): + return render_template("index.html") + + + +# Powerpoint download +@app.route("/pptx", methods=["POST"]) +def pptx(): + # Parse POST parameters + data = json.loads(request.form["data"]) + + # Get lyrics + lyrics = [] + for song in data: + try: + parsedLyrics = models.ParseLyrics(models.GetLyrics(song[1], song[0])) + if (config.parsing["title-slides"]): + lyrics += ["{0}\n{1}".format(song[0], song[1])] + lyrics += parsedLyrics + if (lyrics[-1] != ""): + lyrics += [""] + except: + pass + + try: + # Create powerpoint + temp = tempfile.NamedTemporaryFile(mode="w+t", suffix=".pptx", delete=False) + models.CreatePptx(lyrics, temp.name, False) + temp.close() + + # 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') diff --git a/Songs2Slides/static/index.css b/Songs2Slides/static/index.css @@ -0,0 +1,18 @@ +body { + text-align: center; + font-family: Arial, Helvetica, sans-serif; + touch-action: manipulation; +} + +button { + height: 25px; +} + +#title { + font-size: 25px; + font-weight: bold; +} + +.songRemove { + cursor: pointer; +} +\ No newline at end of file diff --git a/Songs2Slides/static/index.js b/Songs2Slides/static/index.js @@ -0,0 +1,46 @@ +// Declare global variables +setId = 0; // Next valid song id number + + + +// Adds a song +function Add() { + // Create row + var 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", `var element = document.getElementById('song-${setId}'); element.parentNode.removeChild(element);`); + + // Add row + document.getElementById("songs").appendChild(clone); + + // Increment setId + setId++; +} + + + +// Prepares form data for a POST request +function PrepareForm() { + // Get song info + var titles = []; + for (title of document.getElementsByClassName("title")) { + titles.push(title.value); + } + var artists = []; + for (artist of document.getElementsByClassName("artist")) { + artists.push(artist.value); + } + + // Prepare data + data = [] + for (var i = 0; i < titles.length; i++) { + data.push([titles[i], artists[i]]) + } + + // Set data + document.getElementsByName("data")[0].value = JSON.stringify(data) +} +\ No newline at end of file diff --git a/Songs2Slides/templates/index.html b/Songs2Slides/templates/index.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> + <head> + <title>Songs2Slides</title> + <meta name="viewport" content="width=device-width, user-scalable=no"/> + <link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}"></link> + <script src="{{ url_for('static', filename='index.js') }}"></script> + </head> + <body onload="Add();"> + <label id="title">Songs2Slides</label> + <br/> + <label>Create a lyrics powerpoints from a list of songs.</label> + <br/><br/> + + <form action="/pptx" method="POST" onsubmit="PrepareForm();"> + <input hidden name="data"> + <button type="button" onclick="Add();">Add song</button> + <button type="submit">Create Powerpoint</button> + </form> + + + <template id="songTemplate"> + <div> + <input type="text" class="title" placeholder="Song Title"> + <input type="text" class="artist" placeholder="Song Artist"> + <a id="remove" class="songRemove">╳</a> + </div> + </template> + + <br/> + <div id="songs"></div> + </body> +</html> +\ No newline at end of file diff --git a/cliapp.py b/cliapp.py @@ -0,0 +1,104 @@ +# Import dependencies +import json +import os +from Songs2Slides import models, config +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: + parsedLyrics = models.ParseLyrics(models.GetLyrics(artist, title)) + if (config.parsing["title-slides"]): + lyrics += ["{0}\n{1}".format(title, artist)] + lyrics += parsedLyrics + if (lyrics[-1] != ""): + lyrics += [""] + 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) + for line in lyrics: + temp.writelines(line) + temp.writelines("\n\n") + temp.close() + + # Open temp file and wait + subprocess.Popen(["notepad", temp.name]).wait() + + # Read file + with open(temp.name) as f: + rawLines = f.read() + + # Parse lyrics + newLyrics = rawLines.split("\n\n") + del newLyrics[-1] + lyrics = newLyrics + 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: + models.CreatePptx(lyrics, filepath, 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: + os.startfile(filepath) + except: + print("There was an error while opening the powerpoint.") + input("Press enter to exit...") + sys.exit() +\ No newline at end of file diff --git a/requirements.txt b/requirements.txt @@ -1,3 +1,4 @@ bs4 python-pptx -requests -\ No newline at end of file +requests +flask +\ No newline at end of file diff --git a/settings.json b/settings.json @@ -1,23 +0,0 @@ -{ - "title-slides": true, - "lines-per-slide": 4, - - "slide-width": 13.333, - "slide-height": 7.5, - "slide-color": [0, 0, 0], - - "margin-left": 0.5, - "margin-right": 0.5, - "margin-top": 0.5, - "margin-bottom": 0.5, - - "font-family": "Calibri", - "font-size": 40, - "font-bold": false, - "font-italic": false, - "font-color": [255, 255, 255], - - "vertical-alignment": "middle", - "line-spacing": 1.25, - "word-wrap": true -} -\ No newline at end of file diff --git a/webapp.py b/webapp.py @@ -0,0 +1,6 @@ +# Import app +from Songs2Slides import app + +# Run app +if (__name__ == "__main__"): + app.run(debug=True) +\ No newline at end of file