Here's a messy zsh solution, which worked for me on >50,000 songs, where some are mp3 and some are ogg, and about 1000 have embedded lyrics.
#!/usr/bin/zsh
## Extract lyrics from mp3/ogg tags to txt files (suitable for plex media server)
# I couldn't find a tool that worked on both mp3 and ogg, so there are two script sections. Best to run them one at a time
# Both have a commentedout line which actually writes the file. Best to test the scripts on a few different albums before you make a mess of your filesystem
# Both output as follows:
# If there is no lyrics tag, there is no output
# if there is a lyrics tag but it is empty (or almost empty), the output is like this - filename, 0 characters found, and an empty line
#Music/David Rovics/Big Red Sessions/10 - Song for Mavi Mamara.txt
#0
#
# if there are lyrics, the output is like this - filename, large number of characters found, and the complete lyrics as they will be written
#Music/David Rovics/All the News Thats Fit to Sing/16. Sachin.txt
#1421
#Norway is known for many things One of them most surely is not Cricket
#When Norway plays international matches
#They often end up in a sticky wicket
#But VĂ¥lerenga was once one of Norway's best teams
#Until it became one of the worst
#Eighteen games played, sixteen lost It seemed that the team was cursed
#But the men of VĂ¥lerenga have come up with a plan
#And now the course is clear
#Our hope lies now in convincing Sachin That he should fly up here...
#
#
# first section does mp3s, requires eyeD3 : https://eyed3.readthedocs.io/en/latest/
# the eyeD3 plugin which allows you to extract a single tag was removed in 2022
# my messy workaround is to search for the three tags most likely to follow a "Lyrics" tag and ignore the output from there on.
for file in Music/**/*mp3(.); do
if (eyeD3 $file| grep -s -q Lyrics); then
lyrics=$(eyeD3 $file| grep -A999 Lyrics|grep -v FRONT_COVER|grep -v 'Description: cover' |tail -n +2)
if (echo $lyrics| grep -s -q 'UserTextFrame:'); then
lyrics=$(echo $lyrics | head -n+$(echo $lyrics|grep -m1 -n -h UserTextFrame|cut -d: -f1))
fi
if (echo $lyrics| grep -s -q 'OTHER Image:'); then
lyrics=$(echo $lyrics | head -n+$(echo $lyrics|grep -m1 -n -h 'OTHER Image:'|cut -d: -f1))
fi
if (echo $lyrics| grep -s -q 'Description:'); then
lyrics=$(echo $lyrics | head -n+$(echo $lyrics|grep -m1 -n -h 'Description:'|cut -d: -f1))
fi
lyrics=$(echo $lyrics|head -n-1)
echo ${file//mp3/txt} # file name of text file to write lyrics to
echo ${#lyrics} # number of characters in the lyric tag (if its only one or two characters we ignore it)
echo $lyrics # the lyrics which will be written to the file
## if you've tested this on at least a sample of your library, uncomment the next line to write the files
# (( ${#lyrics} > 3 )) && echo $lyrics > ${file//mp3/txt};
fi;
done
second section does oggs, requires tagutil https://github.com/kaworu/tagutil
tagutil can read ogg tags and some mp3 tags.. but not the mp3 lyric tag,
again there is no way to extract a single tag. In my collection the most likely tag to follow the lyrics was either "album artist" or "originaldate"
for file in Music/*/ogg(.); do
if (tagutil $file| grep -q lyrics); then
lyrics=$(tagutil $file|grep -A999 lyrics)
if [[ "$(echo $lyrics)" == "album artist" ]]; then
lyrics=$(echo $lyrics | head -n+$(echo $lyrics|grep -m1 -n -h "album artist"|cut -d: -f1)|head -n-1);
elif [[ "$(echo $lyrics)" == "originaldate" ]]; then
lyrics=$(echo $lyrics | head -n+$(echo $lyrics|grep -m1 -n -h originaldate|cut -d: -f1)|head -n-1);
else
lyrics=${$(tagutil $file|grep -A999 lyrics)//unsyncedlyrics:/}; # if the lyrics are the last tag, this output deals with carriage returns better
fi
lyrics=${lyrics//- lyrics:/}
echo ${file//ogg/txt}; # file name of text file to write lyrics to
echo ${#lyrics} # number of characters in the lyric tag (if its only one or two characters we ignore it)
echo $lyrics # the lyrics which will be written to the file
## if you've tested this on at least a sample of your library, uncomment the next line to write the files
# (( ${#lyrics} > 3 )) && echo $lyrics > ${file//ogg/txt};
fi;
done