From dc50c04da7930b3d5e7164fd2631d5fb0e3be203 Mon Sep 17 00:00:00 2001 From: Jeremy Zongker Date: Wed, 12 Jun 2024 10:31:59 -0500 Subject: [PATCH] Issue #501 Fix (#576) * Show search results * Select songs from search results * Added hymnary * Updated toast message and added error handling. --- package.json | 1 + public/lang/de.json | 2 +- public/lang/en.json | 8 +- public/lang/en_GB.json | 2 +- public/lang/en_ZM.json | 2 +- public/lang/es.json | 2 +- public/lang/it.json | 2 +- public/lang/no.json | 2 +- public/lang/pl.json | 2 +- public/lang/pt_BR.json | 2 +- public/lang/ru.json | 2 +- public/lang/sk.json | 2 +- public/lang/sr.json | 2 +- public/lang/ua.json | 2 +- src/electron/utils/LyricSearch.ts | 153 ++++++++++++++++++ src/electron/utils/responses.ts | 20 +-- .../components/main/popups/CreateShow.svelte | 116 ++++++++++++- 17 files changed, 297 insertions(+), 25 deletions(-) create mode 100644 src/electron/utils/LyricSearch.ts diff --git a/package.json b/package.json index a1112612..8eeedc5f 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "@mapbox/node-pre-gyp": "^1.0.11", "@sveltejs/svelte-virtual-list": "^3.0.1", "@vimeo/player": "^2.16.4", + "axios": "^1.7.2", "chord-transposer": "^3.0.9", "cross-env": "^7.0.3", "electron-store": "^8.0.1", diff --git a/public/lang/de.json b/public/lang/de.json index cde61fd8..e61ac5e5 100644 --- a/public/lang/de.json +++ b/public/lang/de.json @@ -372,7 +372,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/en.json b/public/lang/en.json index 026eb27c..5aec545a 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -394,7 +394,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", @@ -438,7 +438,11 @@ "lyrics": "Lyrics view", "text": "Text edit", "update": "Update show", - "slide_template": "Slide template" + "slide_template": "Slide template", + "search_results": "Search Results", + "source": "Source", + "artist": "Artist", + "song": "Song" }, "actions": { "rename": "Rename", diff --git a/public/lang/en_GB.json b/public/lang/en_GB.json index a24f8906..d58bdb5e 100644 --- a/public/lang/en_GB.json +++ b/public/lang/en_GB.json @@ -387,7 +387,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/en_ZM.json b/public/lang/en_ZM.json index b30d2b9e..e11d27af 100644 --- a/public/lang/en_ZM.json +++ b/public/lang/en_ZM.json @@ -387,7 +387,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/es.json b/public/lang/es.json index 54b01f4c..25534a1c 100644 --- a/public/lang/es.json +++ b/public/lang/es.json @@ -372,7 +372,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/it.json b/public/lang/it.json index d5edc91d..9e2ca20f 100644 --- a/public/lang/it.json +++ b/public/lang/it.json @@ -387,7 +387,7 @@ "no_name": "Nessun nome", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Impossibile trovare alcun testo!", - "lyrics_copied": "Testi copiati da Genius!", + "lyrics_copied": "Testi copiati da ", "no_pdf_linux": "Impossibile esportare come PDF su Linux.", "one_output": "Devi avere almeno un output attivo!", "empty_cache": "La cache è vuota.", diff --git a/public/lang/no.json b/public/lang/no.json index db7b6254..2ede66fa 100644 --- a/public/lang/no.json +++ b/public/lang/no.json @@ -387,7 +387,7 @@ "no_name": "Ingen navn", "media_replaced": "Manglende mediefil erstattet med match.", "lyrics_undefined": "Kunne ikke finne noen sangtekster!", - "lyrics_copied": "Sangtekst kopiert fra Genius!", + "lyrics_copied": "Sangtekst kopiert fra ", "no_pdf_linux": "Kan ikke eksportere som PDF på Linux.", "one_output": "Du må ha minst en aktiv utgang!", "empty_cache": "Hurtigbuffer er tom.", diff --git a/public/lang/pl.json b/public/lang/pl.json index d91c202c..ae013593 100644 --- a/public/lang/pl.json +++ b/public/lang/pl.json @@ -382,7 +382,7 @@ "no_name": "Brak nazwy", "media_replaced": "Brakujący plik zastąpiony pasującym.", "lyrics_undefined": "Nie znaleziono słów do pieśni!", - "lyrics_copied": "Słowa pieśni skopiowano z Genius!", + "lyrics_copied": "Słowa pieśni skopiowano z ", "no_pdf_linux": "Nie można wyeksportować PDF na Linuksie", "one_output": "Musisz mieć co najmniej jedno aktywne wyjście!", "empty_cache": "Pamięć podręczna jest pusta.", diff --git a/public/lang/pt_BR.json b/public/lang/pt_BR.json index aa78665d..1a412cdf 100644 --- a/public/lang/pt_BR.json +++ b/public/lang/pt_BR.json @@ -372,7 +372,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/ru.json b/public/lang/ru.json index bd1307f8..c9729e95 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -382,7 +382,7 @@ "no_name": "Нет имени", "media_replaced": "Отсутствующий файл мультимедиа заменен на совпадение.", "lyrics_undefined": "Не могу найти слова!", - "lyrics_copied": "Текст песни скопирован с Genius!", + "lyrics_copied": "Текст песни скопирован с ", "no_pdf_linux": "Невозможно экспортировать как PDF в Linux.", "one_output": "У вас должен быть хотя бы один активный выход!", "empty_cache": "Кэш пуст.", diff --git a/public/lang/sk.json b/public/lang/sk.json index 0a9e4a14..41fd4942 100644 --- a/public/lang/sk.json +++ b/public/lang/sk.json @@ -372,7 +372,7 @@ "no_name": "Žiadny názov", "media_replaced": "Chýbajúci media súbor nahradený iným.", "lyrics_undefined": "Nenašli sa žiadne texty!", - "lyrics_copied": "Texty skopírované z Genius!", + "lyrics_copied": "Texty skopírované z ", "no_pdf_linux": "Nedá sa exportovať PDF na Linuxe.", "one_output": "Musíte mať aspoň jeden aktívny výstup!", "empty_cache": "Cache je prázdna.", diff --git a/public/lang/sr.json b/public/lang/sr.json index 2190b853..633cebb5 100644 --- a/public/lang/sr.json +++ b/public/lang/sr.json @@ -372,7 +372,7 @@ "no_name": "No name", "media_replaced": "Missing media file replaced with match.", "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from Genius!", + "lyrics_copied": "Lyrics copied from ", "no_pdf_linux": "Can't export as PDF on Linux.", "one_output": "You have to have at least one active output!", "empty_cache": "Cache is empty.", diff --git a/public/lang/ua.json b/public/lang/ua.json index 60d204f1..e55fae6f 100644 --- a/public/lang/ua.json +++ b/public/lang/ua.json @@ -372,7 +372,7 @@ "no_name": "Без імені", "media_replaced": "Відсутній мультимедійний файл замінено відповідним.", "lyrics_undefined": "Не вдалося знайти тексти!", - "lyrics_copied": "Текст пісні скопійовано з Genius!", + "lyrics_copied": "Текст пісні скопійовано з ", "no_pdf_linux": "Неможливо експортувати як PDF у Linux.", "one_output": "Ви повинні мати принаймні один активний вихід!", "empty_cache": "Кеш порожній.", diff --git a/src/electron/utils/LyricSearch.ts b/src/electron/utils/LyricSearch.ts new file mode 100644 index 00000000..5c12c3db --- /dev/null +++ b/src/electron/utils/LyricSearch.ts @@ -0,0 +1,153 @@ +import axios from "axios" + +export type LyricSearchResult = { + source: string, + key: string, + artist: string, + title: string + originalQuery?: string +} + +export class LyricSearch { + + + + static search = async (artist:string, title: string) => { + const results = await Promise.all([ + LyricSearch.searchGenius(artist, title), + LyricSearch.searchHymnary(title) + ]) + return results.flat() + } + + static get(song:LyricSearchResult) { + if (song.source === "Genius") return LyricSearch.getGenius(song) + else if (song.source === "Hymnary") return LyricSearch.getHymnary(song) + return Promise.resolve("") + } + + + //GENIUS + private static getGeniusClient = () => { + const Genius = require("genius-lyrics") + return new Genius.Client() + } + + private static searchGenius = async (artist:string, title: string) => { + try { + const client = this.getGeniusClient() + const songs = await client.songs.search(title + artist) + if (songs.length>3) songs.splice(3, songs.length-3) + return songs.map((s:any) => LyricSearch.convertGenuisToResult(s, title + artist)); + } catch (ex) { + console.log(ex); + return [] + } + } + + //Would greatly prefer to just load via url or id, but the api fails often with these methods (malformed json) + private static getGenius = async (song:LyricSearchResult) => { + const client = this.getGeniusClient() + const songs = await client.songs.search(song.originalQuery || "") + let result = ""; + for (let i = 0; i < songs.length; i++) { + if (songs[i].id.toString() === song.key) { + result = await songs[i].lyrics() + break + } + } + return result + } + + private static convertGenuisToResult = (geniusResult:any, originalQuery:string) => { + return { + source: "Genius", + key: geniusResult.id.toString(), + artist: geniusResult.artist.name, + title: geniusResult.title, + originalQuery: originalQuery + } as LyricSearchResult + } + + //HYMNARY + private static searchHymnary = async (title: string) => { + try { + const url = `https://hymnary.org/search?qu=%20tuneTitle%3A${encodeURIComponent(title)}%20media%3Atext%20in%3Atexts&export=csv` + const response = await axios.get(url) + const csv = await response.data + const songs = LyricSearch.CSVToArray(csv, ",") + if (songs.length>0) songs.splice(0, 1) + for (let i=songs.length-1; i>=0; i--) if (songs[i].length<7) songs.splice(i, 1) + if (songs.length>3) songs.splice(3, songs.length-3) + return songs.map((s:any) => LyricSearch.convertHymnaryToResult(s, title)); + } catch (ex) { + console.log(ex); + return [] + } + } + + private static getHymnary = async (song:LyricSearchResult) => { + const url = `https://hymnary.org/text/${song.key}` + const response = await axios.get(url) + const html = await response.data + const regex = /
(.*?)<\/div>/sg + const match = regex.exec(html) + + let result = "" + if (match) { + result = match[0] + result = result.replaceAll("

", "\n\n") + result = result.replace(/<[^>]*>?/gm, ''); + + const lines = result.split("\n") + const newLines:any[] = [] + lines.forEach((line, idx) => { + if (idx { + return { + source: "Hymnary", + key: hymnaryResult[4], + artist: hymnaryResult[6], + title: hymnaryResult[0], + originalQuery: originalQuery + } as LyricSearchResult + } + + // ref: http://stackoverflow.com/a/1293163/2343 + // This will parse a delimited string into an array of + // arrays. The default delimiter is the comma, but this + // can be overriden in the second argument. + static CSVToArray( strData:string, strDelimiter:string ){ + strDelimiter = (strDelimiter || ","); + + var objPattern = new RegExp(( + "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + + "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + + "([^\"\\" + strDelimiter + "\\r\\n]*))" + ), "gi"); + + var arrData:any[] = [[]]; + var arrMatches = null; + while (arrMatches = objPattern.exec( strData )){ + var strMatchedDelimiter = arrMatches[ 1 ]; + if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) { arrData.push( [] ); } + var strMatchedValue; + if (arrMatches[ 2 ]) strMatchedValue = arrMatches[ 2 ].replace(new RegExp( "\"\"", "g" ), "\""); + else strMatchedValue = arrMatches[ 3 ]; + arrData[ arrData.length - 1 ].push( strMatchedValue ); + } + return( arrData ); + } + +} \ No newline at end of file diff --git a/src/electron/utils/responses.ts b/src/electron/utils/responses.ts index ae6c7a09..2103d29a 100644 --- a/src/electron/utils/responses.ts +++ b/src/electron/utils/responses.ts @@ -40,6 +40,7 @@ import { outputWindows } from "../output/output" import { error_log } from "../data/store" import { startRestListener, startWebSocket, stopApiListener } from "./api" import checkForUpdates from "./updater" +import { LyricSearch } from "./LyricSearch" // IMPORT export function startImport(_e: any, msg: Message) { @@ -115,6 +116,7 @@ const mainResponses: any = { SHOWS_PATH: (): string => getDocumentsFolder(), DATA_PATH: (): string => getDocumentsFolder(null, ""), DISPLAY: (): boolean => false, + GET_LYRICS: (data: any): void => { getLyrics(data) }, GET_MIDI_OUTPUTS: (): string[] => getMidiOutputs(), GET_MIDI_INPUTS: (): string[] => getMidiInputs(), GET_SCREENS: (): void => getScreens(), @@ -127,9 +129,7 @@ const mainResponses: any = { MAXIMIZED: (): boolean => !!mainWindow?.isMaximized(), MINIMIZE: (): void => mainWindow?.minimize(), FULLSCREEN: (): void => mainWindow?.setFullScreen(!mainWindow?.isFullScreen()), - SEARCH_LYRICS: (data: any): void => { - searchLyrics(data) - }, + SEARCH_LYRICS: (data: any): void => { searchLyrics(data) }, SEND_MIDI: (data: any): void => { sendMidi(data) }, @@ -277,13 +277,15 @@ function loadFonts() { // SEARCH_LYRICS async function searchLyrics({ artist, title }: any) { - const Genius = require("genius-lyrics") - const Client = new Genius.Client() - - const songs = await Client.songs.search(title + artist) - const lyrics = songs[0] ? await songs[0].lyrics() : "" + const songs = await LyricSearch.search(artist, title) + toApp("MAIN", { channel: "SEARCH_LYRICS", data: songs }) +} - toApp("MAIN", { channel: "SEARCH_LYRICS", data: { lyrics } }) +// GET_LYRICS +async function getLyrics({song}: any) { + const lyrics = await LyricSearch.get(song) + console.log("****LYRICS", lyrics) + toApp("MAIN", { channel: "GET_LYRICS", data: { lyrics, source:song.source } }) } // GET_SCREENS | GET_WINDOWS diff --git a/src/frontend/components/main/popups/CreateShow.svelte b/src/frontend/components/main/popups/CreateShow.svelte index c0433c95..5c2e02b2 100644 --- a/src/frontend/components/main/popups/CreateShow.svelte +++ b/src/frontend/components/main/popups/CreateShow.svelte @@ -19,6 +19,15 @@ import TextArea from "../../inputs/TextArea.svelte" import TextInput from "../../inputs/TextInput.svelte" import Loader from "../Loader.svelte" + import { get } from "svelte/store" + + type LyricSearchResult = { + source: string + key: string + artist: string + title: string + originalQuery: string + } function textToShow() { let sections = values.text.split("\n\n").filter((a: any) => a.length) @@ -51,6 +60,8 @@ name: "", } + let songs: LyricSearchResult[] | null = null + function keydown(e: any) { if (e.key !== "Enter" || !(e.ctrlKey || e.metaKey)) return @@ -83,6 +94,7 @@ } let showMore: boolean = false + let showSearchResults: boolean = false let activateLyrics: boolean = !!values.text.length let loading = false @@ -98,14 +110,33 @@ loading = true } + function getLyrics(song: LyricSearchResult) { + send(MAIN, ["GET_LYRICS"], { song }) + loading = true + } + // encode using btoa() const blockedWords = ["ZnVjaw==", "Yml0Y2g=", "bmlnZ2E="] let id = "CREATE_SHOW" receive( MAIN, { - SEARCH_LYRICS: (data) => { + SEARCH_LYRICS: (data: LyricSearchResult[]) => { + console.log("DATA IS", data) + loading = false + songs = data + showSearchResults = true + }, + }, + id + ) + receive( + MAIN, + { + GET_LYRICS: (data: { lyrics: string; source: string }) => { + console.log("DATA IS", data) loading = false + showSearchResults = false // filter out songs with bad words blockedWords.forEach((eWord) => { @@ -120,7 +151,7 @@ values.text = data.lyrics activateLyrics = true - newToast("$toast.lyrics_copied") + newToast(get(dictionary).toast?.lyrics_copied + " " + data.source + "!") }, }, id @@ -176,6 +207,49 @@ {/if} +{#if songs !== null} + +{/if} + +{#if showSearchResults} +
+ + + + + + + + + + {#if songs} + {#each songs as song} + + + + + + {/each} + {:else} + + + + {/if} + +
{song.source}{song.artist} + { + getLyrics(song) + }}>{song.title} +
No songs found
+
+{/if} +