Enhanced lyrics flash, then revert to embedded

Issue description:

This may be just something I have set wrong, I have uninstalled and reinstalled, with no effect.

Logs:

Upload description: Telrod11 enhanced lyrics

Additional information:

 
May just be a setting I have wrong
 

Reproduction steps:

 
Being up the player screen
Open lyrics
Quickly jump to the next song

I see the enhanced lyrics flash for just a second, then it reverts to embedded.
 

Media provider:

Local device

Screenshots:

     

Most of the time internal lyrics have priority yes, but it should not blink. Can you make a video?

Sorry, here’s a video…

(I couldn’t find the upload link here quickly this morning, sorry)

I’m guessing that because I’m scrolling through the music so quickly, that it why it flashes the enhanced before setting on the embedded.

I tested with PowerAmp, as it can now use the enhanced lyrics, and they do display natively.

Would you consider allowing the enhanced to be priority, if there are available?

Thanks

The problem is not related to enhanced (means nothing actually) but the source of the lyrics as there’s multiple.

Synced lyrics are prioritized, multiple source is another story.

Support for multiple lyrics is probably the only way to go but will take time.

Ok, understood.

Would this be something you’d like me to make a feature request?

As just a personal question, if I wanted to add all those LRC files I’ve added with onetagger, is there a way I could automate (Windows) and get them in my embedded tags?

Thanks

I think there’s already one similar request.

For the tags I think I’ve some tools named a few times here, maybe someone will jump in.

I have pondered doing this a yearish ago when I noticed that jellyfin did not yet support external lyrics via API (I use jellyfin as provider for Symfonium).
After some further research I found out that this feature is coming (it will be in 10.9.0 which should release within the next weeks) so I did not go through with it (because I prefer using external lyrics) but I can tell you what I would have done.
Note from future me: Writing this down took me way more time than I expected but I’m glad it is done and hope it helps you. :smiley:

Since I have a massive collection and cannot open all songs in mp3tag (the tagging software I use, I’ve never used OneTagger) at the same time, I first decided to specifically only edit files that do have external lyrics. If you have a reasonable collection size where opening it in full is an option, you can skip the filtering steps I’m about to describe (I still recommend them for other reasons tho).

Additionally I wanted to be sure that all .lrc files have a matching .flac or .mp3 song (otherwise the automatic embedding would fail at least partially).

Checking if all .lrc files have matching music files (flac/mp3):

  1. I’ve used Voidtools Everything to search for
    .lrc Z:\
    Z:\ being the root of the mounted music SMB share where all my music is
  2. Then I selected all results via CTRL+A (after checking that it only found .lrc files) and used right click → Copy Full Path on the selection
    Depending on how many .lrc files you have (I have 13.034) this step takes a while.
  3. Then you paste these paths into a new text file named lyrics.txt and save the changes to disk

This is where a python script I wrote for this purpose comes into play (it’s rough but has worked when I tried it). For it to work you obviously have to have python installed on your system.

import os
import re

lyrics = open('lyrics.txt', 'r', encoding="utf8")

lines = lyrics.readlines()

def main():
    for line in lines:
        if os.path.isfile(replext("flac", line)):
            writelog("flac", line)
        elif os.path.isfile(replext("mp3", line)):
            writelog("mp3", line)
        else:
            with open("Fehler.txt", "a", encoding="utf8") as log:
                log.write(line)


def writelog(ext, line):
    with open(f"{ext}.txt", "a", encoding="utf8") as log:
        log.write(re.sub('\.lrc$', f'.{ext}', line))


def replext(ext, line):
    return re.sub('\.lrc$', f'.{ext}', line).strip()

if __name__ == "__main__":
    main()

You can save this as lyricstest.py for example and it has to be put in the same folder where you have created lyrics.txt.

This replaces the .lrc extension from the lyrics.txt paths with .flac or .mp3, checks if the resulting new filepaths exist on your system and logs the results (with .flac/.mp3 extensions) into 3 new .txt files.
flac.txt when it found a matching flac file, mp3.txt when it found a matching .mp3 file and Fehler.txt (I’m German, “Fehler” means error) when it found neither, meaning there isn’t a .flac or .mp3 file linked to the .lrc file.

Broken links can happen when you for example change your file naming scheme for songs without adjusting the .lrc file names at the same time.

To run the script, open the folder where lyrics.txt and lyricstest.py reside in windows explorer, press CTRL+L, type lyricstest.py and press ENTER. That executes the script and should result in 1-3 new text files in the same folder as described above.

If it is created you should check Fehler.txt and investigate why there are no songs linked to the .lrc files (when I first ran this test I found over 200 .lrc files with broken links and fixed them).

To re-run the script after making changes to your files to fix broken links, you should recreate the lyrics.txt file and delete mp3.txt, flac.txt and Fehler.txt before re-running the script to avoid confusion as new results are appended.

Once you are satisfied that all your .lrc files have linked music files it’s mp3tag time

As we now have files containing the paths of all .flac and .mp3 songs which have linked .lrc files, how do we open these (and only these) in mp3tag?

You might have guessed it, I wrote another python script for that.

import subprocess
import os

def main(extensions = ["flac", "mp3"]):
    for ext in extensions:
        if os.path.isfile(f"{ext}.txt"):
            song_list = open(f'{ext}.txt', 'r', encoding="utf8")
            track_path = song_list.readlines()
            for track in track_path:
                try:
                    subprocess.run(["mp3tag", "/add", "/fn:" + track.strip()])
                except subprocess.CalledProcessError:
                    print(f"Error while adding {track} to mp3tag.")
        else:
            print(f"{ext}.txt not found.")
if __name__ == "__main__":
    main()

I called this one mp3tag.py but you can call it whatever you want. This also needs to be in the same folder where flac.txt and mp3.txt are stored to work.

The script checks if flac.txt and mp3.txt exist and if so, opens the songs they contain in mp3tag. As this happens song by song it might take quite a while and lag/flicker but I think I remember testing it with around 9.000 files a year ago and it finished loading eventually.

The neat part is that once all this is done, we have only those songs loaded into mp3tag that do have a matching .lrc file.

WARNING
If you execute the next step, it could lead to data loss as we’re discarding/replacing already embedded lyrics with the contents of the .lrc files.

If you want to reserve the option to later on retrieve the embedded lyrics, you can create an action in mp3tag that saves the current content of the LYRICS tag (I think that’s the tag for synced lyrics) to a custom field like LYRICS_BU.
That way when LYRICS is replaced with the content of the .lrc files you still have a copy of the old embedded tags in LYRICS_BU.

You can save this as backup lyrics.mta in this path (replacing USER with your user name).

C:\Users\USER\AppData\Roaming\Mp3tag\data\actions\
[#0]
T=5
1=%LYRICS%
F=LYRICS_BU


This backs up the current content of LYRICS to LYRICS_BU.

FINAL STEP
Now to the last and possibly destructive action that I stole (and adjusted) from this thread:
You can save this as Import LRC From Filename.mta in this path (replacing USER with your user name).

C:\Users\USER\AppData\Roaming\Mp3tag\data\actions\
[#0]
T=14
F=LYRICS
1=%_filename%.lrc


You can run either of these actions on all selected songs from within mp3tag via
right click → Actions → select from dropdown

If you have followed this rather lengthy and complex guide correctly, you should now have the content of all .lrc files embedded in your songs.
Be aware that there are different tags used for synced / unsynced lyrics in mp3/flac, which is one of the reasons why I prefer having lyrics externally, different conventions for different file types give me a headache.

If you have backed up previous lyrics to LYRICS_BU and wish to delete them you can open all tracks in mp3-tag, press ALT+T, look for the LYRICS_BU field, select it, click the red remove Field X on the right and then click OK to permanently remove the old tags.

Disclaimer:
I’ve written these scripts and actions for my personal use with minimal error handling, so be careful, try to understand everything before executing anything and whatever you do, you do at your own risk.

Holy Cow, thanks so much!!

I’m going to need a bit of time to digest all this, and then back everything up, and give it a shot!!

After reading through my own guide I decided to revisit the subject and simplify it.

import os
import re
import sys
import subprocess
import shutil

extensions = ["flac", "mp3"]

def delete_old_results():
    for ext in extensions:
        result = f"{ext}.txt"
        if os.path.isfile(result):
            os.remove(result)
        if os.path.isfile("lrc_error.log"):
            os.remove("lrc_error.log")

def find_lrc_files(directory="."):
    lrc_files = []
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.endswith(".lrc"):
                if directory == ".":
                    lrc_files.append(os.path.join(os.getcwd(), root, file))
                else:
                    lrc_files.append(os.path.join(root, file))
    return lrc_files

def find_linked_music(ext, song):
    song_path = replext(ext, song)
    if os.path.isfile(song_path):
        return song_path
    else:
        return

def replext(ext, song):
    return re.sub('\.lrc$', f'.{ext}', song)

def writelog(ext, song_path):
    with open(f"{ext}.txt", "a", encoding="utf8") as log:
        log.write(song_path+"\n")

def add_to_mp3tag():
    if shutil.which("mp3tag") is None:
        print("mp3tag is not on PATH, add it and retry.")
        sys.exit(1)
    for ext in extensions:
        if os.path.isfile(f"{ext}.txt"):
            song_list = open(f'{ext}.txt', 'r', encoding="utf8")
            track_path = song_list.readlines()
            for track in track_path:
                try:
                    subprocess.run(["mp3tag", "/add", "/fn:" + track.strip()])
                except subprocess.CalledProcessError:
                    print(f"Error while adding {track} to mp3tag.")
        else:
            print(f"{ext}.txt not found.")

def main(directory="."):
    delete_old_results()
    lrc_paths = find_lrc_files(directory)
    for song in lrc_paths:
        hits = 0
        for ext in extensions:
            song_path = find_linked_music(ext, song)
            if song_path:
                writelog(ext, song_path)
                hits+=1
        if hits == 0:
            with open("lrc_error.log", "a", encoding="utf8") as log:
                log.write(song+"\n")
    if os.path.isfile("lrc_error.log"):
        print("LRC files without linked songs found:")
        with open('lrc_error.log', 'r') as f:
            print(f.read())
        choice = input("Open songs with external lyrics in mp3tag anyhow? (y/n): ")
        if choice == "y":
            add_to_mp3tag()
        else:
            print("Aborting.")
            sys.exit(1)
    else:
        choice = input("Open songs with external lyrics in mp3tag? (y/n): ")
        if choice == "y":
            add_to_mp3tag()
        else:
            print("Aborting.")
            sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) == 1:
        main()
    elif len(sys.argv) == 2:
        directory = sys.argv[1]
        if os.path.isdir(directory):
            main(directory)
        else:
            print("The specified directory does not exist.")
    else:
        print("Wrong argument count. Useage: lyricstest.py directory")

Save this python script as “lyricstest.py”.

You can either run it at the root of your music directory (or for testing purposes in a subdirectory) without argument (if you have write permissions in that folder as the script will create txt files and an error log in the directory you call it in) or you can pass the root of your music directory as an argument to it.

Case 1 when executed at the root of your music folder:

lyricstest.py

Case 2 when executed in a different folder:

lyricstest.py path-to-the-root-of-your-music-folder

It will then scan through that directory and all subdirectories recursively, collect all .lrc files, check if there are songs with the same name in the same directory and log these into text files sorted and named by extension. If it does not find a linked song, it logs the .lrc file to “lrc_error.log” as well as to the console. Upon execution it also deletes old logs to avoid confusion.

You can set which music file extensions the script looks for by editing line 6 of lyricstest.py:

extensions = ["flac", "mp3"]

Example 1:
If you want to add m4a you’d have to add it like this:

extensions = ["flac", "mp3", "m4a"]

Example 2:
If you only have mp3s, remove flac like this:

extensions = ["mp3"]

If the script does not encounter lrc files with missing links it proceeds to add all songs with external .lrc files to mp3tag (it adds them sorted by extension, so the default would be flac first, then mp3). You can re-sort them within mp3tag how you like once all songs are loaded.

The only thing you have to do before running the script is open mp3tag and clear the file list. (CTRL+A, DEL) or (CTRL+A, right click → Remove)

DO NOT use CTRL+A, right click → Delete as that would delete the files from disk instead.

Otherwise the script will open mp3tag with 1 song, wait for the program to close and then open it again with the next song, which is unwanted (if you encounter this case, close the cmd window as well as mp3tag).

The rest of the steps from my earlier guide (everything following WARNING) remain the same.

TLDR version:

  1. Save the script above as lyricstest.py
  2. save the 2 mp3tag actions from the first guide to
    C:\Users\USER\AppData\Roaming\Mp3tag\data\actions\
  3. open mp3tag and clear the file list via CTRL+A, DEL
  4. run the script in the root of your music folder via lyricstest.py
    OR
  5. run the script in a different folder and pass it the root of your music folder via lyricstest.py path-to-the-root-of-your-music-folder
  6. within mp3tag, run the backup lyrics action to back up your existing lyrics of selected songs to LYRICS_BU if desired
  7. within mp3tag, run the Import LRC From Filename action to import the content of the external lyrics to the LYRICS tag

After making sure everything worked you can search for .lrc in your music directory and delete the external lyrics files if you no longer want them.

I’ve tested this version of the script on a couple of test folders, if you encounter errors do let me know and I’ll see if I can fix them.

Disclaimer:
You perform any of these steps at your own risk.

1 Like

I want to thank you for the time and effort you have put into this.

I haven’t gotten to trying it yet,so it’s perfect timing.

I’ll back all my music up before I attempt it, and I’ll let you know if I have any issues.

(May be late next week before I get to it)

Considering how my revised guide still led to some confusion, here’s an even more simplified version of my script that should take care of everything (that I thought of) except for the steps within mp3tag.

Things it handles now:

  • catches missing write permissions
  • handles case of no lrc files in the specified folder
  • catches when mp3tag is not on PATH
  • opens environment variable menu in Windows so the user can add mp3tag to PATH
  • checks if mp3tag actions for import, backup and deletion of the backup exist
  • if an action does not exist, it can create it in the correct folder
  • added a config portion at the top of the script to customize:
  • music file formats to look for
  • custom names for the actions
  • custom tags used for importing and backing up lyrics
  • added -f flag to force overwrite the mp3tag actions (to reflect changed tags in the config)

Edit:

  • added separate mp3tag actions for import to MP3 (SYLT tag as default) and import to FLAC (LYRICS tag as default)
  • adjusted the backup action to back up both SYLT to SYLT_BU and LYRICS to LYRICS_BU (default, custom tags supported)
  • adjusted the backup remove action to delete SYLT_BU and LYRICS_BU (default, custom tags supported)

Anyhow, here’s v3.5 of the script.

import os
import sys
import subprocess
import shutil
import getpass

# CONFIG ##############################################################################

# List of music file types the script will check for
extensions = ["flac", "mp3"]

# Name of the mp3tag action used to import lyrics from external .lrc files
lyrics_import_flac = "LRC#Import LRC From Filename to FLAC"
# Tag to which the lyrics are saved
lyrics_tag_name_flac = "LYRICS"

# Name of the mp3tag action used to import lyrics from external .lrc files
lyrics_import_mp3 = "LRC#Import LRC From Filename to MP3"
# Tag to which the lyrics are saved
lyrics_tag_name_mp3 = "SYLT"

# Name of the mp3tag action used to copy existing embedded lyrics to new tags with _BU appended, default: LYRICS_BU and SYLT_BU 
lyrics_backup = "LRC#Backup Embedded Lyrics"

# Name of the mp3tag action used to remove the backed up tags created with lyrics_backup, default: LYRICS_BU and SYLT_BU
lyrics_backup_remove = "LRC#Remove Lyrics Backup"

# Force overwrite the actions to reflect changed tags, can be set to True on execution via -f flag
overwrite_actions = False

#######################################################################################

def delete_old_results():
    for ext in extensions:
        result = f"{ext}.txt"
        if os.path.isfile(result):
            try:
                os.remove(result)
            except IOError:
                print(f"{result} with previous results cannot be cleared, ensure that you have write permissions and try again.")
                sys.exit(1)
        if os.path.isfile("lrc_error.log"):
            try:
                os.remove("lrc_error.log")
            except IOError:
                print(f"Error log cannot be cleared, ensure that you have write permissions and try again.")
                sys.exit(1)

def find_lrc_files(directory="."):
    lrc_files = []
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.endswith(".lrc"):
                lrc_files.append(os.path.join(os.path.abspath(root), file))
    if len(lrc_files) > 0:
        return lrc_files
    else:
        print("No LRC files found, exiting.")
        sys.exit(1)

def find_linked_music(ext, song):
    song_path = os.path.splitext(song)[0] + f'.{ext}'
    if os.path.isfile(song_path):
        return song_path
    else:
        return

def writelog(ext, song_path):
    try:
        with open(f"{ext}.txt", "a", encoding="utf8") as log:
            log.write(song_path+"\n")
    except IOError:
        print("The file cannot be opened, ensure that you have write permissions and try again.")
        sys.exit(1)

def mp3tag_on_path():
    if shutil.which("mp3tag") is None:
        choice = input("mp3tag is not on PATH, open environment variable settings in Windows to add it? (y/n): ")
        if choice == "y":
            print(f"""    Step 1: Under 'User variables for {getpass.getuser()}' select 'Path' and either double click it or click on 'Edit...'
    Step 2: Check if a path to Mp3tag is among the entries, if not, click on 'New' and paste:
            C:\\Program Files\\Mp3tag
            If you have mp3tag installed in a different directory, add that path instead.
            Note: You can also add the folder that contains this script to PATH in the same way.
    Step 3: Once that is done, re-run this script.""")
            try:
                subprocess.run(["rundll32.exe", "sysdm.cpl,EditEnvironmentVariables"])
            except subprocess.CalledProcessError:
                print(f"Error opening environment variable settings.")
        else:
            print("Aborting.")
            sys.exit(1)
    else:
        return
    
def mp3tag_create_actions():
    action_folder = os.path.join(os.getenv('APPDATA')+"\\Mp3tag\\data\\actions\\")
    # Tag that the embedded lyrics are backed up to
    backup_tags = f"{lyrics_tag_name_flac}_BU;{lyrics_tag_name_mp3}_BU"
    backup_tag_flac = f"{lyrics_tag_name_flac}_BU"
    backup_tag_mp3 = f"{lyrics_tag_name_mp3}_BU"

    back_up_action_path = os.path.join(action_folder + lyrics_backup + ".mta")
    back_up_action_content = f"[#0]\nT=5\n1=%LYRICS%\nF={backup_tag_flac}\n\n[#1]\nT=5\n1=%SYLT%\nF={backup_tag_mp3}\n"

    import_action_path_flac = os.path.join(action_folder + lyrics_import_flac + ".mta")
    import_action_content_flac = f"[#0]\nT=14\nF={lyrics_tag_name_flac}\n1=%_filename%.lrc\n"

    import_action_path_mp3 = os.path.join(action_folder + lyrics_import_mp3 + ".mta")
    import_action_content_mp3 = f"[#0]\nT=14\nF={lyrics_tag_name_mp3}\n1=%_filename%.lrc\n"

    back_up_delete_action_path = os.path.join(action_folder + lyrics_backup_remove + ".mta")
    back_up_delete_action_content = f"[#0]\nT=9\nF={backup_tags}\n"

    if not os.path.isdir(action_folder):
        print(f"{action_folder} not found, skipping action creation.")
        return
    
    mp3tag_create_action(back_up_action_path, lyrics_backup, action_folder, back_up_action_content)
    mp3tag_create_action(import_action_path_flac, lyrics_import_flac, action_folder, import_action_content_flac)
    mp3tag_create_action(import_action_path_mp3, lyrics_import_mp3, action_folder, import_action_content_mp3)
    mp3tag_create_action(back_up_delete_action_path, lyrics_backup_remove, action_folder, back_up_delete_action_content)
        
def mp3tag_create_action(action_path, action_name, action_folder, action_content):
    if not os.path.isfile(action_path) or overwrite_actions:
            choice = ""
            if not overwrite_actions:
                choice = input(f"Create a mp3tag action called '{action_name}' in:\n{action_folder}? (y/n): ")
            if choice == "y" or overwrite_actions == True:
                try:
                    with open(action_path, "w") as text_file:
                        text_file.write(action_content)
                except IOError:
                    print(f"{action_path}.mta cannot be created, ensure that you have write permissions and try again.")
                    sys.exit(1)
                print(f"{action_path} saved.")
            else:
                return

def add_to_mp3tag():
    mp3tag_on_path()
    mp3tag_create_actions()
    for ext in extensions:
        if os.path.isfile(f"{ext}.txt"):
            song_list = open(f'{ext}.txt', 'r', encoding="utf8")
            track_path = song_list.readlines()
            for track in track_path:
                try:
                    subprocess.run(["mp3tag", "/add", "/fn:" + track.strip()])
                except subprocess.CalledProcessError:
                    print(f"Error while adding {track} to mp3tag.")
        else:
            print(f"No {ext} files found.")

def main(directory="."):
    delete_old_results()
    lrc_paths = find_lrc_files(directory)
    for song in lrc_paths:
        hits = 0
        for ext in extensions:
            song_path = find_linked_music(ext, song)
            if song_path:
                writelog(ext, song_path)
                hits+=1
        if hits == 0:
            try:
                with open("lrc_error.log", "a", encoding="utf8") as log:
                    log.write(song+"\n")
            except IOError:
                print("Can't write to error log, ensure that you have write permissions and try again.")
                sys.exit(1)
    if os.path.isfile("lrc_error.log"):
        print("LRC files without linked songs found:")
        with open('lrc_error.log', 'r', encoding="utf8") as f:
            print(f.read())
        choice = input("Open songs with external lyrics in mp3tag anyhow? (y/n): ")
        if choice == "y":
            add_to_mp3tag()
        else:
            print("Aborting.")
            sys.exit(1)
    else:
        choice = input("Open songs with external lyrics in mp3tag? (y/n): ")
        if choice == "y":
            add_to_mp3tag()
        else:
            print("Aborting.")
            sys.exit(1)

if __name__ == "__main__":
    if "-f" in sys.argv:
        overwrite_actions = True
        sys.argv.remove("-f")
    if len(sys.argv) == 1:
        main()
    elif len(sys.argv) == 2:
        directory = sys.argv[1]
        if os.path.isdir(directory):
            main(directory)
        else:
            print("The specified directory does not exist.")
    else:
        print("Wrong argument count. Useage: lyricstest.py [directory] [-f]")

Now the script should tell you what and how to fix until all that’s left to do is run it in the correct folder (or with the correct path as an argument), have mp3tag open, clear the file list, wait for it to add the songs and use the 3 new actions in mp3tag as you see fit.
Considering how limited the mp3tag CLI is, that’s pretty much as easy as I can make it.
If you run into any errors, do let me know.

I hope it helps.

Disclaimer:
As always, you perform these steps at your own risk.

thank you @655321 for your effort

i just wanted to share my setup to add lrc files.
i use this utility to download lrc lyrics, GitHub - tranxuanthang/lrcget: Utility for mass-downloading LRC synced lyrics for your offline music library. for windows/linux/macos
then using mp3tag rightclick action to embed the lyrics to flac or mp3 Embed LRC in FLAC - General Discussion - Mp3tag Community

thanks to navidrome and @Tolriq lyrics are now so easy to access

That looks interesting (though I don’t particularly like the pink design), I’ll check it out.
Edit:
Maybe I’m blind but does lrclib state somewhere which sources it crawls for synced lyrics?
Edit2:
Found this reddit post by the dev that goes into more detail.

This thread is actually the origin of the import action that my script now automatically creates for mp3tag to use (modified to allow for variable target tags).

My script should work fairly well with lrcget.
Download .lrc files with lrcget, then run my script which will only open those songs that actually have external lyrics in mp3tag, embed them via the action and then delete the .lrc files.

Just want to give a shout out to @655321 !!!

I have all my external synced lyrics sorted thanks to this script!

Thank for all your hard work on this!!

2 Likes