Replace musicbrainz with eyed3 for ID tag reading#322
Conversation
read id tags via eyed3 instead of using musicbrainz which is faultprone
Replaced 'musicbrainzngs' with 'eyed3' in dependencies.
📝 WalkthroughWalkthroughThe PR migrates audio metadata extraction from MusicBrainz API lookups to local ID3 tag reading. The ChangesID3 Metadata Extraction via eyed3
Sequence DiagramsequenceDiagram
participant CLI_or_Caller
participant infos_from_audio_video_input_file
participant eyed3
participant Filesystem
CLI_or_Caller->>infos_from_audio_video_input_file: provide input file path
infos_from_audio_video_input_file->>Filesystem: access file bytes
infos_from_audio_video_input_file->>eyed3: eyed3.load(file_path)
eyed3-->>infos_from_audio_video_input_file: ID3 tags (artist, title, year, genre, images)
infos_from_audio_video_input_file->>Filesystem: save cover image (if present)
infos_from_audio_video_input_file-->>CLI_or_Caller: return MediaInfo (artist, title, year, genres, cover_url=None)
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pyproject.toml (1)
25-25: ⚡ Quick winConsider pinning the
eyed3version for reproducible builds.The
eyed3dependency is compatible with the project's Python 3.12-3.13 range (eyed3 0.9.9 requires Python 3.10+). However, while the project currently leaves most dependencies unpinned, pinning versions for all direct dependencies improves build reproducibility and reduces the risk of unexpected breaking changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pyproject.toml` at line 25, The dependency list includes an unpinned "eyed3" entry which can lead to non-reproducible builds; update the pyproject.toml to pin a specific compatible version (e.g., a stable 0.9.x release) for the "eyed3" dependency so installs are deterministic, ensuring the change targets the "eyed3" entry in the project dependencies section of pyproject.toml and updates any lockfile if present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/UltraSinger.py`:
- Around line 674-676: The bare "except Exception:" around the eyed3.load() call
should be replaced with explicit exception types (e.g., IOError, OSError and the
eyed3 library's exception class) and capture the exception into a variable so
you can log it before falling back to defaults; update the except to something
like "except (IOError, OSError, eyed3.Error) as e:" (or the correct eyed3
exception class available in your environment), call your logger with the
exception (e.g., processLogger.error or logging.exception) and then set
file_artist = artist or "Unknown Artist" and file_title = title as the fallback.
- Around line 661-676: The current eyed3-based metadata extraction (eyed3.load)
only handles MP3/ID3 and silently fails for other formats; update the logic in
the metadata block (where eyed3.load is called and file_artist, file_title,
year, genres, cover_image_data are set) to first validate the audio format
(e.g., check file extension or MIME type) and, if not MP3, use a format-agnostic
metadata reader such as mutagen to extract artist/title/year/genre/cover image,
falling back to eyed3 only for MP3; keep the same variables (file_artist,
file_title, year, genres, cover_image_data) and ensure exceptions still fall
back safely to artist/title defaults.
---
Nitpick comments:
In `@pyproject.toml`:
- Line 25: The dependency list includes an unpinned "eyed3" entry which can lead
to non-reproducible builds; update the pyproject.toml to pin a specific
compatible version (e.g., a stable 0.9.x release) for the "eyed3" dependency so
installs are deterministic, ensuring the change targets the "eyed3" entry in the
project dependencies section of pyproject.toml and updates any lockfile if
present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5c1a38a9-e733-4506-816a-2dcddd557343
📒 Files selected for processing (2)
pyproject.tomlsrc/UltraSinger.py
| file_artist, file_title, year, genres, cover_image_data = None, None, None, [], None | ||
|
|
||
| try: | ||
| audio_file = eyed3.load(settings.input_file_path) | ||
| if audio_file is not None and audio_file.tag is not None: | ||
| file_artist = audio_file.tag.artist or artist or "Unknown Artist" | ||
| file_title = audio_file.tag.title or title | ||
| if audio_file.tag.best_release_date: | ||
| year = audio_file.tag.best_release_date.year | ||
| if audio_file.tag.genre: | ||
| genres = [audio_file.tag.genre.name] | ||
| if audio_file.tag.images: | ||
| cover_image_data = audio_file.tag.images[0].image_data | ||
| except Exception: | ||
| file_artist = artist or "Unknown Artist" | ||
| file_title = title |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check which audio formats are referenced in the codebase
echo "=== Searching for audio format references ==="
rg -i --type=py -C2 '(mp3|flac|ogg|m4a|wav|aac)' -g '!test*' | head -30
echo ""
echo "=== Checking eyed3 format support documentation ==="
# Search for format-related checks or conversions
rg -i --type=py 'is_video_file|audio.*format|file.*extension' -C3Repository: rakuri255/UltraSinger
Length of output: 9299
🏁 Script executed:
# First, let's look at the broader context around lines 661-676 in UltraSinger.py
sed -n '600,700p' src/UltraSinger.py | cat -nRepository: rakuri255/UltraSinger
Length of output: 5295
🏁 Script executed:
# Search for where input_file_path is processed before the metadata extraction
rg -n 'input_file_path|eyed3' src/UltraSinger.py | head -40Repository: rakuri255/UltraSinger
Length of output: 1163
🏁 Script executed:
# Check if there's format detection or conversion before metadata extraction
rg -B10 'eyed3.load' src/UltraSinger.pyRepository: rakuri255/UltraSinger
Length of output: 434
🏁 Script executed:
# Check if there's any eyed3 configuration or documentation in the codebase
rg -i 'eyed3|id3' src/ --type=py -B2 -A2Repository: rakuri255/UltraSinger
Length of output: 1406
🏁 Script executed:
# Check requirements/dependencies to see eyed3 version
fd -i requirements '\.(txt|in|toml|yaml|yml)$' -exec cat {} \;Repository: rakuri255/UltraSinger
Length of output: 325
🌐 Web query:
eyed3 library supported audio formats
💡 Result:
The eyeD3 library is primarily designed for working with MP3 files and ID3 metadata [1][2]. According to the official documentation, it explicitly supports the following file types: - MP3 audio files (via eyed3.mp3.Mp3AudioFile) [3] - Raw ID3 data files (via eyed3.id3.TagFile) [3] While the library's design is abstract to allow for potential future support of other formats [1][4], its current core functionality is focused on MP3s and ID3 tags. When using the library's loading functions (such as eyed3.load), it will return None if the file type or MIME type is not recognized [3]. Additionally, eyeD3 supports ID3 metadata versions 1.x (1.0/1.1), 2.2 (read-only), 2.3, and 2.4 [1][5].
Citations:
- 1: https://eyed3.readthedocs.io/en/latest/index.html
- 2: https://eyed3.readthedocs.io/
- 3: https://eyed3.readthedocs.io/en/latest/eyed3.html
- 4: https://eyed3.readthedocs.io/en/latest/readme.html
- 5: https://anaconda.org/conda-forge/eyed3
eyed3 only supports MP3 files with ID3 tags—non-MP3 formats will silently fail to extract metadata.
The eyed3 library is designed exclusively for MP3 files with ID3 tags and returns None for other formats (FLAC, OGG Vorbis, WAV, M4A). When eyed3.load() fails on non-MP3 files, the exception handler falls back to filename-only parsing, causing users to lose available metadata (artist, title, year, genre, cover art) for formats that support tags natively.
Consider adding explicit format validation before attempting ID3 extraction, or use a format-agnostic metadata library for broader audio format support.
🧰 Tools
🪛 Ruff (0.15.12)
[warning] 674-674: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/UltraSinger.py` around lines 661 - 676, The current eyed3-based metadata
extraction (eyed3.load) only handles MP3/ID3 and silently fails for other
formats; update the logic in the metadata block (where eyed3.load is called and
file_artist, file_title, year, genres, cover_image_data are set) to first
validate the audio format (e.g., check file extension or MIME type) and, if not
MP3, use a format-agnostic metadata reader such as mutagen to extract
artist/title/year/genre/cover image, falling back to eyed3 only for MP3; keep
the same variables (file_artist, file_title, year, genres, cover_image_data) and
ensure exceptions still fall back safely to artist/title defaults.
| except Exception: | ||
| file_artist = artist or "Unknown Artist" | ||
| file_title = title |
There was a problem hiding this comment.
Replace bare except Exception: with specific exception types.
The broad exception handler can mask unexpected errors and make debugging difficult. eyed3.load() may raise IOError, OSError, or eyed3-specific exceptions.
🛡️ Proposed fix to catch specific exceptions
- except Exception:
+ except (OSError, IOError, AttributeError):
file_artist = artist or "Unknown Artist"
file_title = title📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except Exception: | |
| file_artist = artist or "Unknown Artist" | |
| file_title = title | |
| except (OSError, IOError, AttributeError): | |
| file_artist = artist or "Unknown Artist" | |
| file_title = title |
🧰 Tools
🪛 Ruff (0.15.12)
[warning] 674-674: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/UltraSinger.py` around lines 674 - 676, The bare "except Exception:"
around the eyed3.load() call should be replaced with explicit exception types
(e.g., IOError, OSError and the eyed3 library's exception class) and capture the
exception into a variable so you can log it before falling back to defaults;
update the except to something like "except (IOError, OSError, eyed3.Error) as
e:" (or the correct eyed3 exception class available in your environment), call
your logger with the exception (e.g., processLogger.error or logging.exception)
and then set file_artist = artist or "Unknown Artist" and file_title = title as
the fallback.
Fix genre assignment to directly use genre name instead of making a list as it breaks parsing in txt converter
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/UltraSinger.py (1)
683-687:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSanitize ID3-derived artist/title before using them in filesystem paths.
Line 683 builds
basename_without_extdirectly from tag values. If tags contain separators or absolute-style prefixes, path creation at Lines 685-687 can escape the intended output layout or fail on invalid names.💡 Suggested fix
+ def _sanitize_path_part(value: str) -> str: + value = value.strip() + value = value.replace(os.sep, " ") + if os.altsep: + value = value.replace(os.altsep, " ") + return value.strip().strip(".") + + safe_artist = _sanitize_path_part(file_artist or "Unknown Artist") + safe_title = _sanitize_path_part(file_title or basename_without_ext) - basename_without_ext = f"{file_artist} - {file_title}" + basename_without_ext = f"{safe_artist} - {safe_title}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/UltraSinger.py` around lines 683 - 687, Sanitize the ID3-derived strings before building basename_without_ext: ensure file_artist and file_title are cleaned to remove path separators, drive/absolute prefixes, invalid filesystem characters and trimmed to a safe length (with a sensible fallback like "unknown_artist" or "unknown_title") before composing basename_without_ext; update the code that sets basename_without_ext and the call sites get_unused_song_output_dir and os_helper.create_folder to use the sanitized values (or add/ reuse a helper function named e.g. sanitize_filename and call it on file_artist and file_title prior to joining with settings.output_folder_path).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/UltraSinger.py`:
- Around line 661-672: The variable genres is initialized as a list but later
assigned a string from eyed3 tags, causing type inconsistency; change the
initialization and all assignments so genres is always a scalar (e.g., None or
an empty string) and set genres = audio_file.tag.genre.name when present,
otherwise genres = None (or ""), and apply the same scalar logic in the later
block referenced around lines 722-726 so consumers of genres always receive the
same type.
---
Outside diff comments:
In `@src/UltraSinger.py`:
- Around line 683-687: Sanitize the ID3-derived strings before building
basename_without_ext: ensure file_artist and file_title are cleaned to remove
path separators, drive/absolute prefixes, invalid filesystem characters and
trimmed to a safe length (with a sensible fallback like "unknown_artist" or
"unknown_title") before composing basename_without_ext; update the code that
sets basename_without_ext and the call sites get_unused_song_output_dir and
os_helper.create_folder to use the sanitized values (or add/ reuse a helper
function named e.g. sanitize_filename and call it on file_artist and file_title
prior to joining with settings.output_folder_path).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
Join multiple genres into a single string if available.
There was a problem hiding this comment.
♻️ Duplicate comments (3)
src/UltraSinger.py (3)
661-672:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGenre extraction is currently discarded at Line 671.
genresis initialized as an empty list, then Line 671 joins that same list, soMediaInfo.genreends upNoneeven when a tag genre exists.Suggested fix
- file_artist, file_title, year, genres, cover_image_data = None, None, None, [], None + file_artist, file_title, year, genre, cover_image_data = None, None, None, None, None @@ - if audio_file.tag.genre: - genres = ', '.join(genres) if genres else None + if audio_file.tag.genre and audio_file.tag.genre.name: + genre = audio_file.tag.genre.name @@ - genre=genres, + genre=genre,Also applies to: 725-725
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/UltraSinger.py` around lines 661 - 672, The code initializes genres as an empty list then immediately sets genres = ', '.join(genres) when audio_file.tag.genre exists, which joins the empty list and results in None; instead, read the tag's genre value into the genres list (e.g., append audio_file.tag.genre.name or str(audio_file.tag.genre)) before joining, and then set the final MediaInfo.genre to the joined string; apply the same fix to the other occurrence around the image-handling block (the second genres handling) so that audio_file.tag.genre is correctly extracted and preserved rather than discarded.
674-676:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNarrow broad exception handling at Line 674.
except Exceptioncan hide unrelated failures; catch expected read/parse errors explicitly and log the error context.#!/bin/bash # Verify no blind exception handlers remain in Python files. rg -nP '^\s*except\s+Exception\s*:' --type=py🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/UltraSinger.py` around lines 674 - 676, Replace the broad "except Exception:" handler around the metadata/read block in UltraSinger.py with explicit exception catches for expected failure modes (e.g., IOError/OSError for file access, ValueError/KeyError/AttributeError for parsing/metadata lookup) and in each except branch set file_artist = artist or "Unknown Artist" and file_title = title, while logging the caught exception and context (include artist/title/file path) using the existing logger; this narrows the scope, preserves fallback assignment for file_artist/file_title, and ensures errors are recorded for debugging.
663-681:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle the
audio_file is Nonepath with filename fallbacks.Fallback values are only set in
except. If metadata loading returnsNonewithout throwing, filename-derivedartist/titleare not preserved, and Line 679 can incorrectly force"Unknown Artist".Suggested fix
- file_artist, file_title, year, genres, cover_image_data = None, None, None, [], None + file_artist, file_title, year, genres, cover_image_data = ( + artist or "Unknown Artist", + title or basename_without_ext, + None, + [], + None, + ) @@ - except Exception: - file_artist = artist or "Unknown Artist" - file_title = title + except Exception: + pass🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/UltraSinger.py` around lines 663 - 681, The try/except currently only sets fallback file_artist/file_title inside the except, so when eyed3.load(settings.input_file_path) returns None you lose filename-derived values; update the logic around eyed3.load and the try block (the block that assigns file_artist and file_title) to explicitly handle audio_file is None by assigning file_artist = artist or "Unknown Artist" and file_title = title or basename_without_ext before falling through, and keep the existing post-checks that default empty values — reference the eyed3.load call, variables file_artist, file_title, settings.input_file_path and basename_without_ext to locate where to add the None-path assignments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/UltraSinger.py`:
- Around line 661-672: The code initializes genres as an empty list then
immediately sets genres = ', '.join(genres) when audio_file.tag.genre exists,
which joins the empty list and results in None; instead, read the tag's genre
value into the genres list (e.g., append audio_file.tag.genre.name or
str(audio_file.tag.genre)) before joining, and then set the final
MediaInfo.genre to the joined string; apply the same fix to the other occurrence
around the image-handling block (the second genres handling) so that
audio_file.tag.genre is correctly extracted and preserved rather than discarded.
- Around line 674-676: Replace the broad "except Exception:" handler around the
metadata/read block in UltraSinger.py with explicit exception catches for
expected failure modes (e.g., IOError/OSError for file access,
ValueError/KeyError/AttributeError for parsing/metadata lookup) and in each
except branch set file_artist = artist or "Unknown Artist" and file_title =
title, while logging the caught exception and context (include artist/title/file
path) using the existing logger; this narrows the scope, preserves fallback
assignment for file_artist/file_title, and ensures errors are recorded for
debugging.
- Around line 663-681: The try/except currently only sets fallback
file_artist/file_title inside the except, so when
eyed3.load(settings.input_file_path) returns None you lose filename-derived
values; update the logic around eyed3.load and the try block (the block that
assigns file_artist and file_title) to explicitly handle audio_file is None by
assigning file_artist = artist or "Unknown Artist" and file_title = title or
basename_without_ext before falling through, and keep the existing post-checks
that default empty values — reference the eyed3.load call, variables
file_artist, file_title, settings.input_file_path and basename_without_ext to
locate where to add the None-path assignments.
Updated genre assignment to use audio file's genre name instead of a list.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/UltraSinger.py (1)
674-676:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNarrow the catch at Line 674 to expected metadata-read failures only.
except Exceptionhides unexpected bugs and silently downgrades metadata quality. Catch specific parse/I/O exceptions and keep fallback behavior.🛠️ Proposed minimal change
- except Exception: + except (OSError, IOError, AttributeError): file_artist = artist or "Unknown Artist" file_title = title#!/bin/bash # Verify broad exception handling still exists in metadata extraction rg -n -C2 'except Exception' src/UltraSinger.py🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/UltraSinger.py` around lines 674 - 676, Replace the broad "except Exception" that sets file_artist and file_title with a narrow catch for expected metadata-read failures: catch specific exceptions such as ValueError, OSError/IOError (and any library-specific parse error like MutagenError if you use mutagen) and keep the same fallback assignment to file_artist = artist or "Unknown Artist" and file_title = title; leave any other unexpected exceptions to bubble up so real bugs aren't silently ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/UltraSinger.py`:
- Around line 685-688: basename_without_ext is built from raw tag values
(file_artist, file_title) and used to create song_folder_output_path; sanitize
file_artist and file_title before composing basename_without_ext to prevent path
separators or invalid filename characters from creating unexpected directories
or OS errors. Update the code that sets basename_without_ext (and any uses of
file_artist/file_title) to run a sanitizer function that strips or replaces path
separators, control characters and OS-invalid filename characters (e.g., replace
"/" "\" ":" "*" "?" "<" ">" "|" and nulls with underscores), trims whitespace,
and optionally truncates to a safe length, then use the sanitized values when
calling os.path.join(settings.output_folder_path, ...) and
get_unused_song_output_dir. Ensure the sanitizer is applied consistently
wherever tag-derived names are used for filesystem paths.
---
Duplicate comments:
In `@src/UltraSinger.py`:
- Around line 674-676: Replace the broad "except Exception" that sets
file_artist and file_title with a narrow catch for expected metadata-read
failures: catch specific exceptions such as ValueError, OSError/IOError (and any
library-specific parse error like MutagenError if you use mutagen) and keep the
same fallback assignment to file_artist = artist or "Unknown Artist" and
file_title = title; leave any other unexpected exceptions to bubble up so real
bugs aren't silently ignored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| basename_without_ext = f"{file_artist} - {file_title}" | ||
|
|
||
| song_folder_output_path = os.path.join(settings.output_folder_path, basename_without_ext) | ||
| song_folder_output_path = get_unused_song_output_dir(song_folder_output_path) |
There was a problem hiding this comment.
Sanitize tag-derived artist/title before using them in filesystem paths.
Line 685 uses raw ID3 values to build directory/file names. Tag values can contain path separators or invalid filename characters, causing path traversal-like folder creation or OS-specific failures.
🛠️ Proposed minimal change
+ invalid_chars = '\\/:*?"<>|'
+ safe_artist = "".join("_" if c in invalid_chars else c for c in file_artist).replace("..", "_").strip().strip(".")
+ safe_title = "".join("_" if c in invalid_chars else c for c in file_title).replace("..", "_").strip().strip(".")
- basename_without_ext = f"{file_artist} - {file_title}"
+ basename_without_ext = f"{safe_artist} - {safe_title}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/UltraSinger.py` around lines 685 - 688, basename_without_ext is built
from raw tag values (file_artist, file_title) and used to create
song_folder_output_path; sanitize file_artist and file_title before composing
basename_without_ext to prevent path separators or invalid filename characters
from creating unexpected directories or OS errors. Update the code that sets
basename_without_ext (and any uses of file_artist/file_title) to run a sanitizer
function that strips or replaces path separators, control characters and
OS-invalid filename characters (e.g., replace "/" "\" ":" "*" "?" "<" ">" "|"
and nulls with underscores), trims whitespace, and optionally truncates to a
safe length, then use the sanitized values when calling
os.path.join(settings.output_folder_path, ...) and get_unused_song_output_dir.
Ensure the sanitizer is applied consistently wherever tag-derived names are used
for filesystem paths.
|
@filipsworks nice work, but:
Use it as second check or as an higher value. |
Read song info + cover from id3 tags instead of using musicbrainz, which makes mismatches
Summary by CodeRabbit
New Features
Chores