From c0c7364e4a6473e4593526389b6b17582206c095 Mon Sep 17 00:00:00 2001 From: Levon Gevorgyan Date: Tue, 5 May 2026 18:03:25 -0500 Subject: [PATCH] add lyricsify --- add-synced-lyrics.sh | 1 + lib/0-utils.sh | 10 ++-- lib/lyricsify.sh | 114 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 lib/lyricsify.sh diff --git a/add-synced-lyrics.sh b/add-synced-lyrics.sh index 8de0591..27ce1df 100755 --- a/add-synced-lyrics.sh +++ b/add-synced-lyrics.sh @@ -56,6 +56,7 @@ process_inputs() { download_with_lrclib download_with_spotify download_with_deezer + download_with_lyricsify ) local numInputs="${#INPUTS[@]}" diff --git a/lib/0-utils.sh b/lib/0-utils.sh index ca3e881..11e26be 100644 --- a/lib/0-utils.sh +++ b/lib/0-utils.sh @@ -95,6 +95,8 @@ check_required_utils() { jq librelyrics spotify # uv tool install --python 3.8 spotify-cli + docker + node ) local missing=false for util in "${utils[@]}"; do @@ -131,11 +133,13 @@ cache_command() { hash="$(sha256sum <<<"${cmd[*]}")" || return 1 read -r cacheFile _ <<<"${hash}" cacheFilePath="${CACHE_DIR}/${cacheFile}" - if [[ -f "${cacheFilePath}" && ${DRY_RUN} == false ]]; then + if [[ -f "${cacheFilePath}" ]]; then + mapfile -t cmdArr <<<"${cmd[*]}" + local skipLines="${#cmdArr[@]}" mapfile -t contents <"${cacheFilePath}" ret=$? - # skip first line since it is the command - printf '%s\n' "${contents[@]:1}" + # skip N lines respective to the command length + printf '%s\n' "${contents[@]:${skipLines}}" else cmdOut="$(dry "${cmd[@]}")" ret=$? diff --git a/lib/lyricsify.sh b/lib/lyricsify.sh new file mode 100644 index 0000000..5acf488 --- /dev/null +++ b/lib/lyricsify.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +FLARESOLVERR_PORT="${FLARESOLVERR_PORT:-8191}" +validate_flaresolverr() { + local checkMsg="$(curl --connect-timeout 1 -s localhost:${FLARESOLVERR_PORT} | jq -r .msg)" + + if [[ "${checkMsg}" == 'FlareSolverr is ready!' ]]; then + return 0 + fi + + debug echo_debug 'flaresolverr not running, attempting to start with docker' + local containerName='flaresolverr' + docker inspect ${containerName} &>/dev/null && docker container rm ${containerName} + + echo_if_fail docker run -d \ + --name=flaresolverr \ + -p ${FLARESOLVERR_PORT}:${FLARESOLVERR_PORT} \ + -e LOG_LEVEL=info \ + --restart unless-stopped \ + ghcr.io/flaresolverr/flaresolverr:latest + local startupRet=$? + + [[ ${startupRet} -ne 0 ]] && return ${startupRet} + + # wait a little to make sure the container is ready + sleep 3 + return ${startupRet} +} + +get_lyricsify_response() { + local isrc="$1" + + validate_flaresolverr || return 1 + + local deezerResponse='' + deezerResponse="$(get_deezer_isrc_response "${isrc}")" || return 1 + + # lyricsify uses artist - song name for URL + # so obtain data from deezer + local deezerMetadata=( + "track .title" + "artist .artist.name" + ) + for data in "${deezerMetadata[@]}"; do + IFS=' ' read -r env jqFilter <<<"${data}" + jqResult="$(jq -r "${jqFilter}" <<<"${deezerResponse}")" + if ! jq_result_valid "${jqResult}"; then + echo_fail "could not determine lyricsify ${env} for ${isrc}" + return 1 + fi + declare "${env}=${jqResult}" + debug echo_debug "${env}=${jqResult}" + done + + # variables assigned above + # shellcheck disable=SC2154 + artist="${artist,,}" + track="${track,,}" + artist="${artist// /-}" + track="${track// /-}" + local lyricsifyUrl="https://www.lyricsify.com/lyrics/${artist}/${track}" + debug echo_debug "using ${lyricsifyUrl}" + + local jsonPayload + jsonPayload="$( + jq -n \ + --arg cmd 'request.get' \ + --arg url "${lyricsifyUrl}" \ + --argjson timeout 60000 \ + '{cmd: $cmd, url: $url, maxTimeout: $timeout}' + )" || return 1 + + cache_command \ + curl \ + -L \ + -X POST \ + "http://localhost:${FLARESOLVERR_PORT}/v1" \ + -H 'Content-Type: application/json' \ + --data-raw "${jsonPayload}" +} + +process_lyricsify_response() { + local response="$1" + + local responseMessage="$(jq -r '.message' <<<"${response}")" + if [[ "${responseMessage}" != 'Challenge solved!' ]]; then + debug echo_fail "could not solve challenge" + return 1 + fi + + local solution="$(jq -r '.solution.response' <<<"${response}")" + + local variableSearch='lrcText' + while read -r line; do + if [[ "${line}" == *"var ${variableSearch} = "* ]]; then + node -e "${line} ; console.log(${variableSearch})" + return $? + fi + done <<<"${solution}" + + return 1 +} + +DOWNLOAD_FUNCTIONS+=(download_with_lyricsify) +download_with_lyricsify() { + local isrc="$1" + local output="$2" + + local lyricsifyResponse syncedLyrics + lyricsifyResponse="$(get_lyricsify_response "${isrc}")" || return 1 + syncedLyrics="$(process_lyricsify_response "${lyricsifyResponse}")" || return 1 + + echo "${syncedLyrics}" >"${output}" +}