add --embed

This commit is contained in:
2026-04-29 17:42:01 -05:00
parent 5ecfd07ad2
commit ffb5cafaf3
4 changed files with 182 additions and 24 deletions
+12 -5
View File
@@ -74,9 +74,13 @@ process_inputs() {
fi fi
output='' output=''
output="$(set_lyric_file_name "${input}")" if ! output="$(set_lyric_file_name "${input}")"; then
echo_fail "could not determine lyric file name for ${input}"
ret=1
continue
fi
if validate_lrc "${output}"; then if validate_song_lyrics "${input}" "${output}"; then
echo_pass "${input} already has lyrics, skipping" echo_pass "${input} already has lyrics, skipping"
continue continue
fi fi
@@ -87,7 +91,7 @@ process_inputs() {
continue continue
fi fi
local tmpOutput="${TMPDIR}/tmp.lrc" local tmpOutput="${TMPDIR}/$(bash_basename "${output}")"
for func in "${downloadFuncs[@]}"; do for func in "${downloadFuncs[@]}"; do
# try to download # try to download
if ! "${func}" "${isrc}" "${tmpOutput}"; then if ! "${func}" "${isrc}" "${tmpOutput}"; then
@@ -105,8 +109,11 @@ process_inputs() {
[[ ${DRY_RUN} == true ]] && continue [[ ${DRY_RUN} == true ]] && continue
if [[ -f "${tmpOutput}" ]]; then if [[ -f "${tmpOutput}" ]]; then
mv "${tmpOutput}" "${output}" &>/dev/null || return 1 add_lyrics_to_song \
echo_pass "added lyrics for ${input}" "${input}" \
"${tmpOutput}" \
"${output}" &&
echo_pass "added lyrics for ${input}"
else else
echo_fail "could not find lyrics for ${input}" echo_fail "could not find lyrics for ${input}"
ret=1 ret=1
+122 -9
View File
@@ -32,7 +32,7 @@ stderr() {
local word="${WORD:-}" local word="${WORD:-}"
local color="${COLOR:-}" local color="${COLOR:-}"
local function="${FUNCNAME[2]}" local function="${FUNCNAME[2]}"
if [[ "${function}" =~ void ]]; then if [[ "${function}" =~ debug|void ]]; then
function="${FUNCNAME[3]}" function="${FUNCNAME[3]}"
fi fi
echo \ echo \
@@ -44,7 +44,7 @@ stderr() {
echo_info() { WORD=INFO COLOR="${CYAN}" stderr "$@"; } echo_info() { WORD=INFO COLOR="${CYAN}" stderr "$@"; }
echo_fail() { WORD=FAIL COLOR="${RED}" stderr "$@"; } echo_fail() { WORD=FAIL COLOR="${RED}" stderr "$@"; }
echo_pass() { WORD=PASS COLOR="${GREEN}" stderr "$@"; } echo_pass() { WORD=PASS COLOR="${GREEN}" stderr "$@"; }
echo_dbug() { WORD=DBUG COLOR="${PURPLE}" stderr "$@"; } echo_debug() { WORD=DBUG COLOR="${PURPLE}" stderr "$@"; }
# only perform the command given if ${DEBUG} is enabled # only perform the command given if ${DEBUG} is enabled
debug() { debug() {
@@ -111,6 +111,20 @@ check_required_utils() {
fi fi
} }
echo_if_fail() {
local cmd=("$@")
local output
output="$("${cmd[@]}" 2>&1)"
local ret=$?
if [[ ${ret} -ne 0 ]]; then
echo_fail "${cmd[*]}"
echo_fail "${output}"
fi
return ${ret}
}
cache_command() { cache_command() {
local cmd=("$@") local cmd=("$@")
local hash cacheFile cmdOut contents ret local hash cacheFile cmdOut contents ret
@@ -149,8 +163,10 @@ check_tmp_dir_empty() {
for f in "${TMPDIR}"/*; do for f in "${TMPDIR}"/*; do
if [[ -f "${f}" ]]; then if [[ -f "${f}" ]]; then
echo_fail "tempdir ${TMPDIR} is not empty!" echo_fail "tempdir ${TMPDIR} is not empty!"
echo_fail "contains ${f}"
echo_fail "aborting operation" echo_fail "aborting operation"
return 1 # execution becomes undefined if TMPDIR is not empty
exit 1
fi fi
done done
@@ -255,10 +271,43 @@ get_file_isrc() {
echo "${jqResult}" echo "${jqResult}"
} }
get_file_lyrics() {
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) | .lyrics'
jqResult="$(jq -r "${jqFilter}" <<<"${probe}")"
if [[ "${jqResult}" == 'null' ]]; then
debug echo_fail "could not find lyrics for ${file}"
return 1
fi
echo "${jqResult}"
}
is_music_file() {
local file="$1"
[[ "${file}" =~ ${MUSIC_EXTENSION_REGEX} ]]
}
is_lrc_file() {
local file="$1"
[[ "${file}" == *'.lrc' ]]
}
set_lyric_file_name() { set_lyric_file_name() {
local file="$1" local file="$1"
if [[ ! "${file}" =~ ${MUSIC_EXTENSION_REGEX} ]]; then if ! is_music_file "${file}"; then
echo "${file} is not a music file" echo_fail "${file} is not a music file"
return 1 return 1
fi fi
@@ -268,19 +317,27 @@ set_lyric_file_name() {
validate_lrc() { validate_lrc() {
local file="$1" local file="$1"
# file does not exist, is invalid # file does not exist, invalid
if [[ ! -f "${file}" ]]; then if [[ ! -f "${file}" ]]; then
return 1 return 1
fi fi
local valid=0 # file is not lyric file, invalid
while read -r line; do if ! is_lrc_file "${file}"; then
return 1
fi
mapfile -t fileContents <"${file}"
local valid=1
for line in "${fileContents[@]}"; do
[[ "${#line}" -eq 0 ]] && continue [[ "${#line}" -eq 0 ]] && continue
if [[ "${line}" != '['* && "${line}" != '#'* ]]; then if [[ "${line}" != '['* && "${line}" != '#'* ]]; then
valid=1 valid=1
break break
fi fi
done <"${file}" valid=0
done
if [[ ${valid} -eq 1 ]]; then if [[ ${valid} -eq 1 ]]; then
debug echo_fail "${file} is not valid lrc file" debug echo_fail "${file} is not valid lrc file"
@@ -289,3 +346,59 @@ validate_lrc() {
return ${valid} return ${valid}
} }
validate_song_lyrics() {
local songFile="$1"
local lyricsDestination="$2"
# embed or not both get validated
validate_lrc "${lyricsDestination}"
local validateDestination=$?
# only continue to validate for embed
if [[ ${EMBED} == false ]]; then
return ${validateDestination}
fi
local tmpLyricsFile="${TMPDIR}/$(bash_basename "${lyricsDestination}")"
get_file_lyrics "${songFile}" >"${tmpLyricsFile}"
validate_lrc "${tmpLyricsFile}"
local validateRet=$?
[[ ${validateRet} -eq 0 ]] && rm "${tmpLyricsFile}"
return ${validateRet}
}
add_lyrics_to_song() {
local songFile="$1"
local lyricsToAdd="$2"
local lyricsDestination="$3"
# this function removes file so be extra certain
is_music_file "${songFile}" &&
is_lrc_file "${lyricsToAdd}" &&
is_lrc_file "${lyricsDestination}" || return 1
if [[ ${EMBED} == false ]]; then
mv "${lyricsToAdd}" "${lyricsDestination}" &>/dev/null
return $?
fi
local tmpSongFile="${TMPDIR}/$(bash_basename "${songFile}")"
echo_if_fail \
ffmpeg \
-hide_banner \
-i "${songFile}" \
-metadata "LYRICS=$(<"${lyricsToAdd}")" \
-c copy \
"${tmpSongFile}"
local ffmpegRet=$?
rm "${lyricsToAdd}"
if [[ ${ffmpegRet} -eq 0 ]]; then
mv "${tmpSongFile}" "${songFile}" &>/dev/null || return 1
if [[ -f "${lyricsDestination}" ]]; then
rm "${lyricsDestination}" || return 1
fi
fi
}
+15 -9
View File
@@ -10,14 +10,15 @@ OPTIONS:
-d, --dir process files in directory -d, --dir process files in directory
-r, --recurse recursively process directory -r, --recurse recursively process directory
-f, --file process one file -f, --file process one file
-e, --embed embed lyrics directly into the song file
-n, --dry-run do not add synced lyrics, -n, --dry-run do not add synced lyrics,
just show the files that would be processed 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 -i, --ignore ignore errors, and continue attempting to find lyrics
even in the case of errors even in the case of errors
-h, --help show this output -h, --help show this output
--debug extra information useful for debugging" --debug extra information useful for debugging
--eval output bash functions that can be directly processed
by eval for use with testing this script."
exit "${EXIT_CODE:-1}" exit "${EXIT_CODE:-1}"
} }
@@ -27,6 +28,7 @@ set_default_options() {
RECURSE=${RECURSE:-false} RECURSE=${RECURSE:-false}
DRY_RUN=${DRY_RUN:-false} DRY_RUN=${DRY_RUN:-false}
IGNORE=${IGNORE:-false} IGNORE=${IGNORE:-false}
EMBED=${EMBED:-false}
DEBUG=${DEBUG:-false} DEBUG=${DEBUG:-false}
# cache # cache
@@ -64,14 +66,10 @@ process_options() {
readonly IGNORE=true readonly IGNORE=true
shift 1 shift 1
;; ;;
--debug) -e | --embed)
readonly DEBUG=true readonly EMBED=true
shift 1 shift 1
;; ;;
-e | --eval)
output_eval_functions
exit 0
;;
-f | --file) -f | --file)
if [[ ! -f "${value}" ]]; then if [[ ! -f "${value}" ]]; then
echo "file ${value} does not exist" echo "file ${value} does not exist"
@@ -80,6 +78,14 @@ process_options() {
readonly FILE="${value}" readonly FILE="${value}"
shift 2 shift 2
;; ;;
--debug)
readonly DEBUG=true
shift 1
;;
--eval)
output_eval_functions
exit 0
;;
-h | --help) -h | --help)
EXIT_CODE=0 usage EXIT_CODE=0 usage
;; ;;
+33 -1
View File
@@ -74,7 +74,7 @@ run_test_cmd() {
fi fi
local output local output
output="$("${cmd[@]}")" output="$("${cmd[@]}" 2>&1)"
local actualRetval=$? local actualRetval=$?
declare -g CMD_OUTPUT="${output}" declare -g CMD_OUTPUT="${output}"
@@ -88,6 +88,10 @@ run_test_cmd() {
} }
test_help_option() {
add-synced-lyrics --help
}
test_bad_input() { test_bad_input() {
# incomplete args # incomplete args
run_test_cmd \ run_test_cmd \
@@ -114,6 +118,11 @@ test_bad_input() {
1 \ 1 \
add-synced-lyrics --file "${PROGPATH}" --dir "${PROGDIR}" --recurse add-synced-lyrics --file "${PROGPATH}" --dir "${PROGDIR}" --recurse
# non-existent flag
run_test_cmd \
1 \
add-synced-lyrics --this-flag-does-not-exist
return 0 return 0
} }
@@ -174,23 +183,43 @@ test_determine_inputs_find_recurse() {
} }
test_file_dry() { test_file_dry() {
setup_test_data
add-synced-lyrics --file "${GOOD_SONG}" --dry-run add-synced-lyrics --file "${GOOD_SONG}" --dry-run
} }
test_directory_dry() { test_directory_dry() {
setup_test_data
add-synced-lyrics --dir "${TEST_DIR}" --dry-run add-synced-lyrics --dir "${TEST_DIR}" --dry-run
} }
test_directory_recurse_dry() { test_directory_recurse_dry() {
setup_test_data
add-synced-lyrics --dir "${TEST_DIR}" --recurse --dry-run add-synced-lyrics --dir "${TEST_DIR}" --recurse --dry-run
} }
test_good_file() { test_good_file() {
setup_test_data
add-synced-lyrics --file "${GOOD_SONG}" --debug add-synced-lyrics --file "${GOOD_SONG}" --debug
test -f "${GOOD_LYRICS}" test -f "${GOOD_LYRICS}"
} }
test_good_file_embed() {
test_good_file
# rm "${GOOD_LYRICS}"
add-synced-lyrics --file "${GOOD_SONG}" --embed
run_test_cmd \
1 \
test -f "${GOOD_LYRICS}"
# do it again to test that embedded is picked up and valid
run_test_cmd \
0 \
add-synced-lyrics --file "${GOOD_SONG}" --embed
[[ "${CMD_OUTPUT}" == *'already has lyrics, skipping'* ]]
}
test_bad_file() { test_bad_file() {
setup_test_data
run_test_cmd \ run_test_cmd \
1 \ 1 \
add-synced-lyrics \ add-synced-lyrics \
@@ -248,6 +277,7 @@ setup_test_data() {
} }
TESTS=( TESTS=(
test_help_option
test_determine_inputs test_determine_inputs
test_determine_inputs_recurse test_determine_inputs_recurse
test_determine_inputs_find test_determine_inputs_find
@@ -257,6 +287,7 @@ TESTS=(
test_directory_recurse_dry test_directory_recurse_dry
test_file_dry test_file_dry
test_good_file test_good_file
test_good_file_embed
test_bad_file test_bad_file
test_directory_recurse test_directory_recurse
test_directory_recurse_ignore test_directory_recurse_ignore
@@ -376,5 +407,6 @@ if [[ $# -eq 0 ]]; then
echo echo
analyze_coverage analyze_coverage
else else
setup_coverage
"$@" "$@"
fi fi