add caching to commands

This commit is contained in:
2026-04-25 19:09:19 -05:00
parent bddc943de5
commit eeba67f6f6
8 changed files with 728 additions and 351 deletions
+1
View File
@@ -1 +1,2 @@
test-dir*
*.json
+73 -327
View File
@@ -1,118 +1,10 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
set -u
have_cmd() {
for cmd in "$@"; do
command -v "${cmd}" &>/dev/null || return 1
done
}
# shellcheck disable=SC2329
void() {
echo "$@" &>/dev/null
}
# shellcheck disable=SC2329
dry() {
local args=("$@")
local numArgs="${#args[@]}"
test "${numArgs}" -eq 0 && return 0
echo "'${args[0]}'"
local earlyStop=$((numArgs - 1))
for ((i = 1; i < earlyStop; i++)); do
echo " '${args[${i}]}' \\"
done
echo " '${args[${i}]}'"
}
check_required_utils() {
local utils=(
ffmpeg
jq
syncedlyrics
librelyrics
spotify # uv tool install --python 3.8 spotify-cli
)
local missing=false
for util in "${utils[@]}"; do
if ! have_cmd "${util}"; then
echo "missing ${util}!"
missing=true
fi
done
if [[ ${missing} == true ]]; then
return 1
else
return 0
fi
}
# shellcheck disable=SC2329
get_spotify_url() {
local search="$1"
local response artistAlbumTitle url
response="$(
spotify \
search \
--track "${search}" \
--limit 1 \
--raw
)"
artistAlbumTitle="$(
jq -r '.items[0] | "\(.artists[0].name) - \(.album.name) - \(.name)"' \
<<<"${response}"
)"
if [[ "${artistAlbumTitle}" != "${search}" ]]; then
echo "${search} could not be found"
return 1
fi
url="$(
jq -r '.items[0] | "\(.external_urls.spotify)"' <<<"${response}"
)"
if [[ -z "${url}" ]]; then
echo "could not determine url for ${search}"
return 1
fi
echo "${url}"
}
output_eval_functions() {
while read -r line; do
IFS=' ' read -r _ _ function <<<"${line}"
[[ "${function}" == '_determine_inputs_'* ||
"${function}" == set_default_options ]] || continue
${line}
done <<<"$(declare -F)"
}
get_artist_album_title() {
local file="$1"
local jqFilter='.streams[] | select(.codec_type == "audio") | .tags | "\(.ARTIST) - \(.ALBUM) - \(.TITLE)"'
ffprobe \
-v error \
-show_entries stream \
-of json \
"${file}" | jq \
-r "${jqFilter}"
}
set_lyric_file_name() {
local file="$1"
if [[ ! "${file}" =~ ${MUSIC_EXTENSION_REGEX} ]]; then
echo "${file} is not a music file"
return 1
fi
echo "${file%.opus}.lrc"
}
bash_basename() {
local tmp
path="$1"
@@ -125,245 +17,99 @@ bash_basename() {
printf '%s\n' "${tmp:-/}"
}
readonly PROGNAME="$(bash_basename "$0")"
bash_dirname() {
# Usage: dirname "path"
local tmp=${1:-.}
usage() {
echo "${PROGNAME} [options]
[[ $tmp != *[!/]* ]] && {
printf '/\n'
return
}
OPTIONS:
-d, --dir process files in directory
-r, --recurse recursively process directory
-f, --file process one file
-n, --dry-run do not actually add synced lyrics,
just show the files that would be processed
-e, --eval output bash functions that can be directly processed
by eval for use with testing this script.
-i, --ignore ignore errors, and continue attempting to find lyrics
even in the case of errors
-h, --help show this output"
exit ${EXIT_CODE:-1}
tmp=${tmp%%"${tmp##*[!/]}"}
[[ $tmp != */* ]] && {
printf '.\n'
return
}
tmp=${tmp%/*}
tmp=${tmp%%"${tmp##*[!/]}"}
printf '%s\n' "${tmp:-/}"
}
set_default_options() {
DIR=${DIR:-}
FILE=${FILE:-}
RECURSE=${RECURSE:-false}
DRY_RUN=${DRY_RUN:-false}
IGNORE=${IGNORE:-false}
readonly MUSIC_EXTENSION_REGEX='.*\.(opus)$'
}
PROGPATH="$(readlink -f "$0")"
PROGDIR="$(bash_dirname "${PROGPATH}")"
PROGNAME="$(bash_basename "${PROGPATH}")"
# shellcheck disable=SC2034
readonly PROGPATH PROGDIR PROGNAME || exit 1
process_options() {
while [[ $# -gt 0 ]]; do
option="$1"
value="${2:-}"
case "${option}" in
-d | --dir)
if [[ ! -d "${value}" ]]; then
echo "directory ${value} does not exist"
usage
fi
readonly DIR="${value}"
shift 2
;;
-r | --recurse)
readonly RECURSE=true
shift 1
;;
-n | --dry-run)
readonly DRY_RUN=true
shift 1
;;
-i | --ignore)
readonly IGNORE=true
shift 1
;;
-e | --eval)
output_eval_functions
exit 0
;;
-f | --file)
if [[ ! -f "${value}" ]]; then
echo "file ${value} does not exist"
usage
fi
readonly FILE="${value}"
shift 2
;;
-h | --help)
EXIT_CODE=0 usage
;;
*)
echo "bad option"
usage
;;
esac
done
if [[ "${DIR:-}${FILE:-}" == '' ]]; then
echo "either --dir or --file must be used"
usage
fi
if [[ -n "${DIR}" && -n "${FILE}" ]]; then
echo "cannot process both directory and file"
usage
fi
if [[ "${RECURSE}" == true && -n "${FILE}" ]]; then
echo "--recurse can only be used with --dir"
usage
fi
}
_determine_inputs_fd() {
local fdCmd=(
fd
--type f
"${MUSIC_EXTENSION_REGEX}"
"${DIR}"
)
if [[ "${RECURSE}" == false ]]; then
fdCmd+=(--max-depth 1)
fi
mapfile -t INPUTS < <("${fdCmd[@]}" | sort)
}
_determine_inputs_find() {
local findCmd=(
find
"${DIR}"
)
if [[ "${RECURSE}" == false ]]; then
findCmd+=(-maxdepth 1)
fi
findCmd+=(
-regextype posix-extended
-regex "${MUSIC_EXTENSION_REGEX}"
-type f
)
mapfile -t INPUTS < <("${findCmd[@]}" | sort)
}
_determine_inputs_bash() {
local unsortedInputs=()
if [[ "${RECURSE}" == true ]]; then
local preGlobstar="$(shopt -p globstar)"
shopt -s globstar
for f in "${DIR}"/**/*; do
unsortedInputs+=("${f}")
done
${preGlobstar}
else
for f in "${DIR}"/**; do
unsortedInputs+=("${f}")
done
fi
local filteredInputs=()
for input in "${unsortedInputs[@]}"; do
[[ "${input}" =~ ${MUSIC_EXTENSION_REGEX} ]] || continue
filteredInputs+=("${input}")
done
mapfile -t INPUTS < <(printf '%s\n' "${filteredInputs[@]}" | sort)
}
determine_inputs() {
INPUTS=()
if [[ -n "${FILE}" ]]; then
INPUTS=("${FILE}")
elif have_cmd fd; then
_determine_inputs_fd || return 1
elif have_cmd find; then
_determine_inputs_find || return 1
else
_determine_inputs_bash || return 1
fi
if [[ "${#INPUTS[@]}" -eq 0 ]]; then
echo "no inputs found, something is wrong"
exit 1
fi
readonly INPUTS
}
create_tmp_dir() {
TMPDIR="$(mktemp -d)" || return 1
readonly TMPDIR
trap 'rm -rf ${TMPDIR}' EXIT
}
for lib in "${PROGDIR}/lib/"*; do
# shellcheck disable=SC1090
source "${lib}" || exit 1
done
process_inputs() {
local dry='' void=''
if [[ "${DRY_RUN}" == true ]]; then
dry=dry
void=void
fi
local isrc output ret=0
local downloadFuncs=(
download_with_lrclib
download_with_spotify
download_with_deezer
)
local search='' output='' spotifyUrl='' ret=''
for input in "${INPUTS[@]}"; do
if [[ ${IGNORE} == false && ${ret} -ne 0 ]]; then
return 1
fi
if [[ "${input,,}" == *'instrumental'* ]]; then
if [[ "${input,,}" == *'instrumental'* ||
"${input,,}" == *'acoustic'* ]]; then
echo "${input} is instrumental, skipping"
continue
fi
output=''
output="$(set_lyric_file_name "${input}")"
if [[ ! -f "${output}" ]]; then
echo "processing ${input}"
search="$(get_artist_album_title "${input}")"
# try with syncedlyrics first
${dry} syncedlyrics \
--synced-only \
--output "${output}" \
"${search}"
if ! ${void} test -f "${output}"; then
for f in "${TMPDIR}"/*; do
if [[ -f "${f}" ]]; then
echo "tempdir ${TMPDIR} is not empty!"
echo "aborting operation"
return 1
fi
done
# try with librelyrics-spotify next
if ! spotifyUrl="$(${dry} get_spotify_url "${search}")"; then
ret=1
continue
fi
if ! ${dry} librelyrics \
--directory "${TMPDIR}" \
"${spotifyUrl}"; then
ret=1
continue
fi
if ! ${dry} mv "${TMPDIR}"/*.lrc "${output}"; then
ret=1
continue
fi
fi
if ! ${void} test -f "${output}"; then
echo "failed to get lyrics for ${input}"
ret=1
if [[ -f "${output}" ]]; then
if is_valid_lrc "${output}"; then
continue
else
rm "${output}" || return 1
fi
fi
echo "processing ${input}"
isrc=''
if ! isrc="$(get_file_isrc "${input}")"; then
ret=1
continue
fi
local tmpOutput="${TMPDIR}/tmp.lrc"
for func in "${downloadFuncs[@]}"; do
"${func}" "${isrc}" "${tmpOutput}" && break
done
if [[ -f "${tmpOutput}" ]]; then
if is_valid_lrc "${tmpOutput}"; then
mv "${tmpOutput}" "${output}" &>/dev/null || return 1
else
rm "${tmpOutput}" || return 1
fi
fi
if ! void test -f "${output}"; then
echo "failed to get lyrics for ${input}"
ret=1
continue
else
void echo "added lyrics for ${input}"
fi
done
return ${ret}
}
check_required_utils || exit 1
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
# check commands exist
# "$@" commands to check
have_cmd() {
for cmd in "$@"; do
command -v "${cmd}" &>/dev/null || return 1
done
}
# validate results from `jq -r`
jq_result_valid() {
local result="$1"
if [[ -z "${result}" || "${result}" == 'null' ]]; then
return 1
fi
return 0
}
stderr() { echo "$*" 1>&2; }
info() { stderr "INFO [${FUNCNAME[1]}]: $*"; }
error() { stderr "ERROR [${FUNCNAME[1]}]: $*"; }
# only echo "$@" if ${DEBUG} is true
debug() {
if [[ ${DEBUG} == false ]]; then
return 0
fi
stderr "$@"
}
# do not do anything
# if ${DRY_RUN} is enabled
void() {
local cmd=("$@")
if [[ ${DRY_RUN} == true ]]; then
return 0
else
"${cmd[@]}"
return $?
fi
}
# print out "$@" formatted for bash
# if ${DRY_RUN} is enabled
# otherwise, execute the actual command
dry() {
local cmd=("$@")
if [[ ${DRY_RUN} == true ]]; then
local numCmd="${#cmd[@]}"
test "${numCmd}" -eq 0 && return 0
local firstLine="'${cmd[0]}'"
test "${numCmd}" -gt 1 && firstLine+=' \'
stderr "${firstLine}"
local earlyStop=$((numCmd - 1))
for ((i = 1; i < earlyStop; i++)); do
stderr " '${cmd[${i}]}' \\"
done
stderr " '${cmd[${i}]}'"
else
"${cmd[@]}"
return $?
fi
}
# check required utilities for this project
check_required_utils() {
local utils=(
base64
cat
ffprobe
jq
librelyrics
spotify # uv tool install --python 3.8 spotify-cli
)
local missing=false
for util in "${utils[@]}"; do
if ! have_cmd "${util}"; then
error "missing ${util}!"
missing=true
fi
done
if [[ ${missing} == true ]]; then
return 1
else
return 0
fi
}
cache_command() {
local cmd=("$@")
local hash cacheFile cmdOut contents ret
hash="$(sha256sum <<<"${cmd[*]}")" || return 1
read -r cacheFile _ <<<"${hash}"
cacheFilePath="${CACHE_DIR}/${cacheFile}"
if [[ -f "${cacheFilePath}" && ${DRY_RUN} == false ]]; then
mapfile -t contents <"${cacheFilePath}"
ret=$?
# skip first line since it is the command
printf '%s\n' "${contents[@]:1}"
else
cmdOut="$(dry "${cmd[@]}")"
ret=$?
if [[ ${ret} -eq 0 && ${DRY_RUN} == false ]]; then
{
echo "${cmd[*]}"
echo "${cmdOut}"
} >"${cacheFilePath}" || return 1
echo "${cmdOut}"
fi
fi
return "${ret}"
}
create_tmp_dir() {
TMPDIR="$(mktemp -d)" || return 1
readonly TMPDIR
trap 'rm -rf ${TMPDIR}' EXIT
}
check_tmp_dir_empty() {
for f in "${TMPDIR}"/*; do
if [[ -f "${f}" ]]; then
error "tempdir ${TMPDIR} is not empty!"
error "aborting operation"
return 1
fi
done
return 0
}
# librelyrics only exposes a directory to download to
# so this wrapper function downloads to a directory
# and then outputs the contents of the downloaded file
# to stdout so it can effectively be cached
librelyrics_dl() {
local url="$1"
check_tmp_dir_empty || return 1
librelyrics \
--directory "${TMPDIR}" \
"${url}" 1>&2
local librelyricsRet=$?
local downloadedFile=''
for f in "${TMPDIR}/"*; do
if [[ -f "${f}" ]]; then
downloadedFile="${f}"
break
fi
done
if [[ ${librelyricsRet} -eq 0 && -z ${downloadedFile} ]]; then
error "failed to download file for ${url} despite no librelyrics error"
return 1
fi
local fileContents=''
if [[ -n "${downloadedFile}" ]]; then
fileContents="$(<"${downloadedFile}")"
rm "${downloadedFile}" || return 1
fi
if [[ ${librelyricsRet} -eq 0 && -z ${fileContents} ]]; then
error "failed to read file for ${url} despite no librelyrics error"
return 1
fi
echo "${fileContents}"
}
download_with_librelyrics() {
local isrc="$1"
local output="$2"
local urlType="$3"
if [[ ! "${urlType}" =~ spotify|deezer ]]; then
error "only spotify|deezer are supported"
return 1
fi
local url=''
url="$(get_"${urlType}"_url "${isrc}")" || return 1
local lyrics=''
lyrics="$(cache_command \
librelyrics_dl \
"${url}")"
if [[ -z "${lyrics}" ]]; then
error "could not get lyrics for ${isrc} using ${urlType}"
return 1
fi
info "found lyrics for ${isrc} with ${urlType}"
echo "${lyrics}" >"${output}"
}
output_eval_functions() {
while read -r line; do
IFS=' ' read -r _ _ function <<<"${line}"
[[ "${function}" == '_determine_inputs_'* ||
"${function}" == set_default_options ]] || continue
${line}
done <<<"$(declare -F)"
}
get_file_isrc() {
local file="$1"
local probe='' jqResult=''
local probe=''
probe="$(
ffprobe \
-v error \
-show_entries stream \
-of json \
"${file}"
)" || return 1
local jqFilter='.streams[] | select(.codec_type == "audio") | .tags | with_entries(.key |= ascii_downcase) | .isrc'
jqResult="$(jq -r "${jqFilter}" <<<"${probe}")"
if [[ "${jqResult}" == 'null' ]]; then
error "failed to find ISRC for ${file}"
return 1
fi
echo "${jqResult}"
}
set_lyric_file_name() {
local file="$1"
if [[ ! "${file}" =~ ${MUSIC_EXTENSION_REGEX} ]]; then
echo "${file} is not a music file"
return 1
fi
echo "${file%.opus}.lrc"
}
is_valid_lrc() {
local file="$1"
test -f "${file}" || return 1
local valid=0
while read -r line; do
if [[ "${line}" != '['* ]]; then
valid=1
break
fi
done <"${file}"
return ${valid}
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
get_deezer_isrc_response() {
local isrc="$1"
if [[ -z "${isrc}" ]]; then
error "missing isrc"
return 1
fi
cache_command \
curl \
--silent \
--retry 3 \
"https://api.deezer.com/track/isrc:${isrc}"
}
get_deezer_url() {
local isrc="$1"
if [[ -z "${isrc}" ]]; then
error "missing isrc"
return 1
fi
local deezerResponse=''
deezerResponse="$(get_deezer_isrc_response "${isrc}")" || return 1
local jqResult=''
jqResult="$(jq -r .link <<<"${deezerResponse}")"
if ! jq_result_valid "${jqResult}"; then
error "could not get link from deezer for ${isrc}"
return 1
fi
echo "${jqResult}"
}
download_with_deezer() {
local isrc="$1"
local output="$2"
download_with_librelyrics \
"${isrc}" \
"${output}" \
deezer
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
get_lrclib_api_response() {
local isrc="$1"
local deezerResponse=''
deezerResponse="$(get_deezer_isrc_response "${isrc}")" || return 1
# get metadata from deezer
local deezerMetadata=(
"track .title"
"artist .artist.name"
"album .album.title"
"duration .duration"
)
for data in "${deezerMetadata[@]}"; do
IFS=' ' read -r env jqFilter <<<"${data}"
jqResult="$(jq -r "${jqFilter}" <<<"${deezerResponse}")"
if ! jq_result_valid "${jqResult}"; then
error "could not determine lrclib ${env} for ${isrc}"
return 1
fi
declare "${env}=${jqResult}"
debug "${env}=${jqResult}"
done
# variables assigned above
# shellcheck disable=SC2154
cache_command \
curl \
"https://lrclib.net/api/get" \
--silent \
--get \
--data-urlencode "track_name=${track}" \
--data-urlencode "artist_name=${artist}" \
--data-urlencode "album_name=${album}" \
--data-urlencode "duration=${duration}"
}
download_with_lrclib() {
local isrc="$1"
local output="$2"
# get lrclib ID
local lrclibResponse syncedLyrics
lrclibResponse="$(get_lrclib_api_response "${isrc}")"
syncedLyrics="$(jq -r .syncedLyrics <<<"${lrclibResponse}")"
if ! jq_result_valid "${syncedLyrics}"; then
error "could not get lyrics for ${isrc}"
return 1
fi
echo "${syncedLyrics}" >"${output}"
}
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
usage() {
echo "${PROGNAME} [options]
OPTIONS:
-d, --dir process files in directory
-r, --recurse recursively process directory
-f, --file process one file
-n, --dry-run do not actually add synced lyrics,
just show the files that would be processed
-e, --eval output bash functions that can be directly processed
by eval for use with testing this script.
-i, --ignore ignore errors, and continue attempting to find lyrics
even in the case of errors
-h, --help show this output
--debug extra information useful for debugging"
exit "${EXIT_CODE:-1}"
}
set_default_options() {
DIR=${DIR:-}
FILE=${FILE:-}
RECURSE=${RECURSE:-false}
DRY_RUN=${DRY_RUN:-false}
IGNORE=${IGNORE:-false}
DEBUG=${DEBUG:-false}
# cache
CACHE_HOME="${XDG_CACHE_HOME:-"${HOME}/.cache"}"
CACHE_DIR="${CACHE_HOME}/add-synced-lyrics"
if [[ ! -d "${CACHE_DIR}" ]]; then
mkdir "${CACHE_DIR}" || return 1
fi
readonly MUSIC_EXTENSION_REGEX='.*\.(opus)$'
}
process_options() {
while [[ $# -gt 0 ]]; do
option="$1"
value="${2:-}"
case "${option}" in
-d | --dir)
if [[ ! -d "${value}" ]]; then
echo "directory ${value} does not exist"
usage
fi
readonly DIR="${value}"
shift 2
;;
-r | --recurse)
readonly RECURSE=true
shift 1
;;
-n | --dry-run)
readonly DRY_RUN=true
shift 1
;;
-i | --ignore)
readonly IGNORE=true
shift 1
;;
--debug)
readonly DEBUG=true
shift 1
;;
-e | --eval)
output_eval_functions
exit 0
;;
-f | --file)
if [[ ! -f "${value}" ]]; then
echo "file ${value} does not exist"
usage
fi
readonly FILE="${value}"
shift 2
;;
-h | --help)
EXIT_CODE=0 usage
;;
*)
echo "bad option"
usage
;;
esac
done
if [[ "${DIR:-}${FILE:-}" == '' ]]; then
echo "either --dir or --file must be used"
usage
fi
if [[ -n "${DIR}" && -n "${FILE}" ]]; then
echo "cannot process both directory and file"
usage
fi
if [[ "${RECURSE}" == true && -n "${FILE}" ]]; then
echo "--recurse can only be used with --dir"
usage
fi
}
_determine_inputs_fd() {
local fdCmd=(
fd
--type f
"${MUSIC_EXTENSION_REGEX}"
"${DIR}"
)
if [[ "${RECURSE}" == false ]]; then
fdCmd+=(--max-depth 1)
fi
mapfile -t INPUTS < <("${fdCmd[@]}" | sort)
}
_determine_inputs_find() {
local findCmd=(
find
"${DIR}"
)
if [[ "${RECURSE}" == false ]]; then
findCmd+=(-maxdepth 1)
fi
findCmd+=(
-regextype posix-extended
-regex "${MUSIC_EXTENSION_REGEX}"
-type f
)
mapfile -t INPUTS < <("${findCmd[@]}" | sort)
}
_determine_inputs_bash() {
local unsortedInputs=()
local pattern="${DIR}/*"
shopt -s nullglob globstar || return 1
[[ "${RECURSE}" == true ]] && pattern="${DIR}/**/*"
mapfile -t unsortedInputs < <(printf '%s\n' ${pattern})
local filteredInputs=()
for input in "${unsortedInputs[@]}"; do
[[ "${input}" =~ ${MUSIC_EXTENSION_REGEX} ]] || continue
filteredInputs+=("${input}")
done
mapfile -t INPUTS < <(printf '%s\n' "${filteredInputs[@]}" | sort)
}
determine_inputs() {
INPUTS=()
if [[ -n "${FILE}" ]]; then
INPUTS=("${FILE}")
elif have_cmd fd; then
_determine_inputs_fd || return 1
elif have_cmd find; then
_determine_inputs_find || return 1
else
_determine_inputs_bash || return 1
fi
if [[ "${#INPUTS[@]}" -eq 0 ]]; then
echo "no inputs found, something is wrong"
exit 1
fi
readonly INPUTS
}
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# many functions are invoked via variables
# shellcheck disable=SC2329
get_spotify_url() {
local isrc="$1"
if [[ -z "${isrc}" ]]; then
error "missing isrc"
return 1
fi
local search="isrc:${isrc}"
local response
response="$(
cache_command \
spotify \
search \
--track \
"${search}" \
--limit 1 \
--raw
)" || return 1
local tags=(
spotifyISRC
spotifyURL
)
# unset tag values
for tag in "${tags[@]}"; do
declare "${tag}="
done
local jqFilter='.items[] | "\(.external_ids.isrc)|\(.external_urls.spotify)"'
local jqOutput="$(jq -r "${jqFilter}" <<<"${response}")"
# compare tags versus the reference for each line
local foundMatching=false
while read -r line; do
IFS='|' read -r "${tags[@]}" <<<"${line}"
for tag in "${tags[@]}"; do
declare -n spotifyTagValue="${tag}"
debug "${tag}=${spotifyTagValue}"
done
# shellcheck disable=SC2154
if [[ "${spotifyISRC,,}" == "${isrc,,}" ]]; then
foundMatching=true
break
fi
done <<<"${jqOutput}"
if [[ "${foundMatching}" == false ]]; then
error "could not find matching spotify track for ${isrc}"
return 1
fi
# shellcheck disable=SC2154
echo "${spotifyURL}"
return 0
}
download_with_spotify() {
local isrc="$1"
local output="$2"
download_with_librelyrics \
"${isrc}" \
"${output}" \
spotify
}
+39 -23
View File
@@ -7,8 +7,12 @@ set -u
readonly THIS_DIR="$(dirname "$(readlink -f "$0")")"
readonly TEST_DIR="${THIS_DIR}/test-dir"
readonly TEST_FILE_1="${TEST_DIR}/03 - Back Burner.opus"
readonly TEST_FILE_2="${TEST_DIR}/subdir/05 - Composure.opus"
# lyrics exist for this song
readonly GOOD_SONG="${TEST_DIR}/back burner.opus"
readonly GOOD_LYRICS="${GOOD_SONG//.opus/.lrc}"
# lyrics do not exist for this song
readonly BAD_SONG="${TEST_DIR}/subdir/avalanche.opus"
readonly BAD_LYRICS="${BAD_SONG//.opus/.lrc}"
readonly PROG="${THIS_DIR}/add-synced-lyrics.sh"
test_bad_input() {
@@ -59,31 +63,42 @@ test_determine_inputs_recurse() {
}
test_file_dry() {
"${PROG}" --file "${TEST_FILE_1}" --dry-run || return 1
return 0
"${PROG}" --file "${GOOD_SONG}" --dry-run
}
test_directory_dry() {
"${PROG}" --dir "${TEST_DIR}" --dry-run || return 1
return 0
"${PROG}" --dir "${TEST_DIR}" --dry-run
}
test_directory_recurse_dry() {
"${PROG}" --dir "${TEST_DIR}" --recurse --dry-run || return 1
"${PROG}" --dir "${TEST_DIR}" --recurse --dry-run
}
test_good_file() {
"${PROG}" --file "${GOOD_SONG}" || return 1
test -f "${GOOD_LYRICS}"
}
test_bad_file() {
"${PROG}" --file "${BAD_SONG}" && return 1
! test -f "${BAD_LYRICS}"
}
test_directory_recurse() {
setup_test_data || return 1
# this test should fail since it includes the bad file
"${PROG}" --dir "${TEST_DIR}" --recurse && return 1
return 0
}
test_file() {
"${PROG}" --file "${TEST_FILE_1}" || return 1
test -f "${TEST_FILE_1//.opus/.lrc}"
}
test_directory_recurse_ignore() {
setup_test_data || return 1
"${PROG}" --dir "${TEST_DIR}" --recurse --ignore && return 1
test -f "${GOOD_LYRICS}" || return 1
test -f "${BAD_LYRICS}" && return 1
test_directory_recurse() {
"${PROG}" --dir "${TEST_DIR}" --recurse || return 1
test -f "${TEST_FILE_2//.opus/.lrc}"
return 0
}
setup_test_data() {
@@ -96,15 +111,13 @@ setup_test_data() {
-i anullsrc=channel_layout=stereo:sample_rate=48000
-c:a libopus
-t 5
-metadata ALBUM="Messengers"
-metadata ARTIST="August Burns Red"
)
"${ffmpegCmd[@]}" \
-metadata TITLE="Back Burner" \
"${TEST_FILE_1}" || return 1
-metadata ISRC=USTN10700139 \
"${GOOD_SONG}" || return 1
"${ffmpegCmd[@]}" \
-metadata TITLE="Composure" \
"${TEST_FILE_2}" || return 1
-metadata ISRC=US5261822978 \
"${BAD_SONG}" || return 1
}
TESTS=(
@@ -115,8 +128,10 @@ TESTS=(
test_directory_dry
test_directory_recurse_dry
test_file_dry
test_file
test_good_file
test_bad_file
test_directory_recurse
test_directory_recurse_ignore
)
# make sure no test is accidentally missed
@@ -135,6 +150,7 @@ for test in "${TESTS[@]}"; do
"${test}"
) 2>&1
)"; then
echo
echo "!! ${test} failed !!"
echo "${testOutput}"
exit 1