Skip to main content

Overview

TagQt integrates with MusicBrainz, the open music encyclopedia, to automatically fetch accurate metadata for your audio files.
Auto-tagging requires the musicbrainzngs Python package. TagQt uses the official MusicBrainz API with rate limiting to respect their usage guidelines.

MusicBrainz Integration

The MusicBrainzClient class handles all interactions with the MusicBrainz API:
# From tagqt/core/musicbrainz.py - API setup
import musicbrainzngs

musicbrainzngs.set_useragent("TagQt", "1.0", "https://github.com/example/tagqt")
musicbrainzngs.set_rate_limit(limit_or_interval=1.0, new_requests=1)

Auto-Tagging Workflow

1

Select Files

Select files to auto-tag (single or multiple files)
2

Choose Operation Mode

  • Skip Tagged Files - Only process files missing metadata
  • Process All Files - Update all files regardless of existing tags
3

Search MusicBrainz

TagQt searches using existing artist and album information
4

Apply Metadata

Matched metadata is automatically applied and saved

Starting Auto-Tag

Access auto-tagging from multiple locations:
  • Menu - Edit → Auto-Tag → Selected / All Visible
  • Context Menu - Right-click → Auto-Tag (Selected)
  • Command Palette - Ctrl+K → “Auto-Tag (All)“
# From tagqt/ui/main.py - Auto-tag dialog
def _autotag_files(self, files):
    msg = QMessageBox(self)
    msg.setWindowTitle("Auto-Tag Options")
    msg.setText(f"Auto-tag {len(files)} files from MusicBrainz?")
    msg.setInformativeText("Choose how to handle files that already have tags:")
    
    skip_btn = msg.addButton("Skip Tagged Files", QMessageBox.AcceptRole)
    all_btn = msg.addButton("Process All Files", QMessageBox.ActionRole)
    cancel_btn = msg.addButton("Cancel", QMessageBox.RejectRole)

Search Algorithm

The MusicBrainz client uses a sophisticated search and matching process:
# From tagqt/core/musicbrainz.py - Searching releases
@classmethod
def search_release(cls, artist, album, track_title=None):
    if not artist and not album:
        return None
    
    def do_search():
        return musicbrainzngs.search_releases(
            artist=artist,
            release=album,
            limit=10
        )
    
    data = cls._retry(do_search)
    if not data:
        return None
        
    releases = data.get("release-list", [])
    if not releases:
        return None
    
    # Prioritize official releases
    official = [r for r in releases if r.get("status") == "Official"]
    best = official[0] if official else releases[0]

Title Normalization

To improve matching accuracy, titles are normalized:
# From tagqt/core/musicbrainz.py - Title matching
@staticmethod
def normalize_title(title):
    if not title:
        return ""
    normalized = unicodedata.normalize('NFKD', title)
    normalized = normalized.encode('ascii', 'ignore').decode('ascii')
    # Remove parenthetical content
    normalized = re.sub(r'\s*\([^)]*\)\s*$', '', normalized)
    # Remove bracketed content
    normalized = re.sub(r'\s*\[[^\]]*\]\s*$', '', normalized)
    # Remove quotes/apostrophes
    normalized = re.sub(r"['\`]", "", normalized)
    # Remove punctuation
    normalized = re.sub(r'[^\w\s]', '', normalized)
    normalized = ' '.join(normalized.lower().split())
    return normalized

@classmethod
def titles_match(cls, title1, title2):
    if not title1 or not title2:
        return False
    n1 = cls.normalize_title(title1)
    n2 = cls.normalize_title(title2)
    if n1 == n2:
        return True
    if n1 in n2 or n2 in n1:
        return True
    return False

Retrieved Metadata

Auto-tagging fetches comprehensive metadata:

Basic Information

  • Album title
  • Artist name
  • Release date/year
  • Country
  • Release status

Track Details

  • Track position
  • Disc number
  • Track count
  • Disc count

Genre Information

  • Release genres
  • Release group genres
  • Artist genres
  • Sorted by popularity

Identifiers

  • Release ID
  • Release Group ID
  • Artist ID

Genre Fallback Hierarchy

TagQt uses a smart fallback system for genres:
# From tagqt/core/musicbrainz.py - Genre fallback
# 1. Try release-level genres
tags = best.get("tag-list", [])
if tags:
    sorted_tags = sorted(tags, key=lambda x: int(x.get("count", 0)), reverse=True)
    result["genres"] = [t.get("name", "") for t in sorted_tags[:3]]

# 2. Fallback to release group genres
if not result["genres"] and result.get("release_group_id"):
    rg_genres = cls.lookup_release_group(result["release_group_id"])
    if rg_genres:
        result["genres"] = rg_genres

# 3. Fallback to artist genres
if not result["genres"] and result.get("artist_id"):
    artist_genres = cls.lookup_artist(result["artist_id"])
    if artist_genres:
        result["genres"] = artist_genres

Release Lookup

For detailed track information, TagQt performs release lookups:
# From tagqt/core/musicbrainz.py - Release lookup
@classmethod
def lookup_release(cls, release_id, track_title=None):
    if not release_id:
        return None
        
    def do_lookup():
        return musicbrainzngs.get_release_by_id(
            release_id,
            includes=["recordings", "media", "tags", "release-groups"]
        )
    
    data = cls._retry(do_lookup)
    if not data:
        return None
        
    release = data.get("release", {})
    
    # Extract disc count
    media = release.get("medium-list", [])
    result["disc_count"] = len(media)
    
    # Find matching track
    if track_title and media:
        for disc_num, disc in enumerate(media, 1):
            tracks = disc.get("track-list", [])
            for track in tracks:
                recording = track.get("recording", {})
                rec_title = recording.get("title", "")
                if cls.titles_match(track_title, rec_title):
                    result["track_disc"] = disc_num
                    result["track_position"] = int(track.get("position", 0))
                    result["track_count"] = len(tracks)

Error Handling and Retry Logic

The client includes robust error handling:
# From tagqt/core/musicbrainz.py - Retry mechanism
@classmethod
def _retry(cls, func, max_retries=3):
    import requests
    for attempt in range(max_retries):
        try:
            return func()
        except (musicbrainzngs.NetworkError, requests.exceptions.RequestException, OSError) as e:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            print(f"MusicBrainz network error after {max_retries} retries: {e}")
            return None
        except musicbrainzngs.MusicBrainzError as e:
            print(f"MusicBrainz API error: {e}")
            return None
    return None
MusicBrainz enforces rate limiting. TagQt automatically handles this with 1 request per second, but large batch operations may take time.

Batch Auto-Tagging

Process multiple files with progress tracking:
# From tagqt/ui/main.py - Batch auto-tag
def _autotag_files(self, files):
    # ... dialog handling ...
    
    if not self._prepare_batch("Auto-Tag Status"):
        return
        
    self.progress_bar.setRange(0, len(files))
    self.progress_bar.setFormat("Auto-tagging... 0%")
    
    self._start_batch_worker(
        AutoTagWorker(files, skip_existing=skip_existing), 
        connect_log=True
    )
The AutoTagWorker processes files sequentially, reporting progress:
  • Success - Metadata found and applied
  • Skipped - File already has tags (when skip mode enabled)
  • Not Found - No match in MusicBrainz
  • Error - Network or API error

Best Practices

  • Ensure files already have Artist and Album tags
  • Use Skip Tagged Files for initial tagging
  • Use Process All Files to refresh/update existing metadata
  • Check the batch status dialog for any failed matches
  • Manually edit artist/album names for better search results
  • Try alternative spellings or romanized names
  • Check MusicBrainz.org directly for correct release information
  • TagQt respects MusicBrainz rate limits (1 req/sec)
  • Large batches will take time - this is intentional
  • Don’t cancel operations unnecessarily

Additional Lookups

The client supports additional MusicBrainz lookups:

Release Group Lookup

# From tagqt/core/musicbrainz.py
@classmethod
def lookup_release_group(cls, rg_id):
    if not rg_id:
        return []
        
    def do_lookup():
        return musicbrainzngs.get_release_group_by_id(rg_id, includes=["tags"])
    
    data = cls._retry(do_lookup)
    if not data:
        return []
        
    rg = data.get("release-group", {})
    tags = rg.get("tag-list", [])
    if tags:
        sorted_tags = sorted(tags, key=lambda x: int(x.get("count", 0)), reverse=True)
        return [t.get("name", "") for t in sorted_tags[:3]]
    return []

Artist Lookup

# From tagqt/core/musicbrainz.py
@classmethod
def lookup_artist(cls, artist_id):
    if not artist_id:
        return []
        
    def do_lookup():
        return musicbrainzngs.get_artist_by_id(artist_id, includes=["tags"])
    
    data = cls._retry(do_lookup)
    if not data:
        return []
        
    artist = data.get("artist", {})
    tags = artist.get("tag-list", [])
    if tags:
        sorted_tags = sorted(tags, key=lambda x: int(x.get("count", 0)), reverse=True)
        return [t.get("name", "") for t in sorted_tags[:3]]
    return []

Edit Menu

  • Auto-Tag → Selected - Tag selected files
  • Auto-Tag → All Visible - Tag all visible files

Command Palette (Ctrl+K)

  • “Auto-Tag (All)” - Tag all visible files

Context Menu

  • Right-click selected files → Auto-Tag (Selected)

Next Steps

Batch Operations

Learn more about batch processing

Cover Art

Automatically download album artwork

Build docs developers (and LLMs) love