Skip to main content

Overview

TagQt provides comprehensive cover art management, including downloading from multiple sources, embedding in audio files, and exporting to image files.

Cover Art Sources

The CoverArtManager fetches artwork from two primary sources:

MusicBrainz/Cover Art Archive

High-quality official release artwork from the Cover Art Archive

iTunes API

Album artwork from Apple’s iTunes Store database
# From tagqt/core/art.py - CoverArtManager initialization
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class CoverArtManager:
    ITUNES_API_URL = "https://itunes.apple.com/search"

    def __init__(self):
        self.session = requests.Session()
        retries = Retry(
            total=3, 
            backoff_factor=1, 
            status_forcelist=[500, 502, 503, 504]
        )
        self.session.mount('https://', HTTPAdapter(max_retries=retries))
        self.session.mount('http://', HTTPAdapter(max_retries=retries))

Downloading Cover Art

For single files, use the interactive search dialog:
1

Click Cover Button

In the sidebar, click the “Get Cover” button
2

Search Sources

TagQt searches MusicBrainz and iTunes using Artist and Album tags
3

Preview Results

Browse multiple cover art candidates with previews
4

Select and Apply

Choose a cover and it’s automatically embedded in the file
# From tagqt/ui/main.py - Single file cover search
def search_cover(self):
    if getattr(self.sidebar, 'is_global_mode', False):
        self.fetch_all_covers()
        return
        
    artist = self.sidebar.artist_edit.text()
    album = self.sidebar.album_edit.text()
    
    from tagqt.ui.search import UnifiedSearchDialog
    
    def search_callback(a, al):
        return self.cover_manager.search_cover_candidates(a, al)
        
    dialog = UnifiedSearchDialog(
        self, 
        mode="cover", 
        initial_artist=artist, 
        initial_album=album, 
        fetcher_callback=search_callback
    )
    
    if dialog.exec() == QDialog.Accepted and dialog.selected_result:
        res = dialog.selected_result
        url = res.get("url")
        
        try:
            data = self.cover_manager.download_and_process_cover(url)
            if data:
                pixmap = QPixmap()
                pixmap.loadFromData(data)
                self.sidebar.set_cover(pixmap)
                
                if self.metadata:
                    self.metadata.set_cover(data)
        except Exception as e:
            dialogs.show_error(self, "Error", f"Error downloading cover: {e}")

Batch Cover Download

Download covers for multiple files:
  • Menu - Tools → Covers → Get Covers (All Visible)
  • Context Menu - Right-click selection → Get Covers (Selected)
  • Command Palette - Ctrl+K → “Get Covers (All)“
# From tagqt/ui/main.py - Batch cover fetch
def fetch_all_covers(self):
    if getattr(self.sidebar, 'is_global_mode', False):
        files = self.get_selected_files()
    else:
        files = self.get_all_files()
    self._fetch_covers_list(files)

def _fetch_covers_list(self, files):
    if not files:
        dialogs.show_warning(self, "No Files", "No files loaded.")
        return
        
    if not self._prepare_batch("Get Covers Status"):
        return
        
    self.progress_bar.setRange(0, len(files))
    self.progress_bar.setFormat("Processing... 0%")
    
    self._start_batch_worker(CoverFetchWorker(files, self.cover_manager))

Source Priority

The cover art manager uses intelligent source prioritization:
# From tagqt/core/art.py - Source priority
def search_cover(self, artist, album):
    # Try MusicBrainz first, then iTunes
    url = self.search_cover_musicbrainz(artist, album)
    if url:
        return url
        
    return self.search_cover_itunes(artist, album)

MusicBrainz/Cover Art Archive

# From tagqt/core/art.py - MusicBrainz search
def search_cover_musicbrainz(self, artist, album):
    def do_search():
        query = f'artist:"{artist}" AND release:"{album}"'
        url = "https://musicbrainz.org/ws/2/release-group"
        params = {
            "query": query,
            "fmt": "json"
        }
        headers = {"User-Agent": "tagqt/1.0 ( [email protected] )"}
        
        response = self.session.get(url, params=params, headers=headers, timeout=10)
        response.raise_for_status()
        return response.json()

    data = self._retry(do_search)
    if data and data.get("release-groups"):
        rg_id = data["release-groups"][0]["id"]
        # Get Cover Art from Cover Art Archive
        cover_url = f"https://coverartarchive.org/release-group/{rg_id}/front"
        return cover_url
    return None

iTunes API

# From tagqt/core/art.py - iTunes search
def search_cover_itunes(self, artist, album):
    cands = self.search_cover_candidates(artist, album)
    if cands:
        return cands[0]["url"]
    return None

Cover Art Candidates

The search returns multiple candidates for manual selection:
# From tagqt/core/art.py - Getting candidates
def search_cover_candidates(self, artist, album):
    """Returns a list of cover candidates."""
    candidates = []
    
    # iTunes
    try:
        params = {
            "term": f"{artist} {album}",
            "media": "music",
            "entity": "album",
            "limit": 5  # Get multiple results
        }
        response = self.session.get(self.ITUNES_API_URL, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
        
        for item in data.get("results", []):
            url = item.get("artworkUrl100")
            if url:
                # Upgrade to high resolution
                url = url.replace("100x100bb", "1000x1000bb")
                candidates.append({
                    "album": item.get("collectionName"),
                    "artist": item.get("artistName"),
                    "url": url,
                    "source": "iTunes",
                    "size": "1000x1000"
                })
    except Exception as e:
        print(f"Error searching iTunes candidates: {e}")
        
    # MusicBrainz
    try:
        mb_url = self.search_cover_musicbrainz(artist, album)
        if mb_url:
            candidates.append({
                "album": album,
                "artist": artist,
                "url": mb_url,
                "source": "MusicBrainz",
                "size": "Unknown"
            })
    except Exception:
        pass
        
    return candidates

Image Processing

Cover art is automatically processed when downloaded:
# From tagqt/core/art.py - Download and process
def download_and_process_cover(self, url):
    def do_download():
        response = self.session.get(url, timeout=15)
        response.raise_for_status()
        return response.content

    content = self._retry(do_download)
    if not content:
        return None

    try:
        img = Image.open(BytesIO(content))
        img = img.convert("RGB")
        img = img.resize((500, 500), Image.Resampling.LANCZOS)
        
        output = BytesIO()
        img.save(output, format="JPEG", quality=90)
        return output.getvalue()
    except Exception as e:
        print(f"Error processing cover: {e}")
        return None
Covers are automatically resized to 500x500 pixels and converted to JPEG format for optimal file size and compatibility.

Embedding Cover Art

Covers are embedded using format-specific methods:
# From tagqt/core/tags.py - Setting cover art
def set_cover(self, data, max_size=500):
    """Sets the cover art, optionally resizing it."""
    if self.audio is None or not data:
        return

    # Resize if needed
    if max_size and max_size > 0:
        try:
            img = Image.open(io.BytesIO(data))
            
            if img.width > max_size or img.height > max_size:
                img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
                
                output = io.BytesIO()
                img = img.convert('RGB')
                img.save(output, format='JPEG', quality=85)
                data = output.getvalue()
        except Exception as e:
            print(f"Error resizing cover: {e}")

    try:
        # ID3 (MP3)
        if isinstance(self.audio, (ID3, EasyID3)):
            tags = self.audio if isinstance(self.audio, ID3) else self.audio.tags
            if tags is not None:
                # Remove existing
                to_del = [k for k in tags.keys() if k.startswith('APIC:')]
                for k in to_del:
                    del tags[k]
                
                tags.add(APIC(
                    encoding=3,
                    mime='image/jpeg',
                    type=3,
                    desc='Cover',
                    data=data
                ))
        
        # FLAC
        elif isinstance(self.audio, (FLAC, OggVorbis)):
            self.audio.clear_pictures()
            p = Picture()
            p.data = data
            p.type = 3
            p.mime = "image/jpeg"
            self.audio.add_picture(p)
        
        # MP4
        elif isinstance(self.audio, MP4):
            self.audio['covr'] = [MP4Cover(data, imageformat=MP4Cover.FORMAT_JPEG)]
    except Exception as e:
        print(f"Error setting cover: {e}")

Extracting Cover Art

Read embedded cover art from files:
# From tagqt/core/tags.py - Getting cover art
def get_cover(self):
    if self.audio is None:
        return None

    try:
        # ID3 (MP3)
        if isinstance(self.audio, (ID3, EasyID3)):
            tags = self.audio
            if hasattr(tags, '_ID3__id3'):  # EasyID3 internal
                tags = tags._ID3__id3
            
            for key in tags.keys():
                if key.startswith('APIC:'):
                    return tags[key].data
        
        # FLAC / Ogg
        if isinstance(self.audio, (FLAC, OggVorbis)):
            if hasattr(self.audio, 'pictures') and self.audio.pictures:
                return self.audio.pictures[0].data

        # MP4
        elif isinstance(self.audio, MP4):
            if 'covr' in self.audio and self.audio['covr']:
                return bytes(self.audio['covr'][0])

    except Exception as e:
        print(f"Error getting cover: {e}")
    return None

Exporting to Files

Covers are automatically exported to cover.jpg when saving:
# From tagqt/core/tags.py - Saving cover to file
def save_cover_file(self, data=None, overwrite=True):
    """
    Saves cover art to cover.jpg in the same directory.
    If data is provided, uses it. Otherwise tries to get from tags.
    If overwrite is False, checks if file exists first.
    """
    try:
        if data is None:
            data = self.get_cover()
        
        if not data:
            return

        dir_path = os.path.dirname(self.filepath)
        cover_path = os.path.join(dir_path, "cover.jpg")
        
        if not overwrite and os.path.exists(cover_path):
            return
            
        with open(cover_path, 'wb') as f:
            f.write(data)
    except Exception as e:
        print(f"Error saving cover file: {e}")

Embedded Covers

Stored inside audio files
  • Portable with the file
  • Displayed in music players
  • No external dependencies

cover.jpg Files

External image files
  • Easy to view/edit
  • Used by media servers
  • Shared across album

Format-Specific Tags

FormatTag/FrameType
MP3APICAttached Picture frame
FLACpicturesPicture block
OGGpicturesPicture block
M4A/MP4covrCover art atom

Resizing Covers

Resize embedded cover art to reduce file size:

Single File

  • Automatic when downloading (500x500, quality 90)
  • Automatic when setting from file (500x500, quality 85)

Batch Resize

  • Menu - Tools → Covers → Resize Covers (Selected/All Visible)
  • Context Menu - Right-click → Resize Covers (Selected)
  • Command Palette - Ctrl+K → “Resize Covers”
# From tagqt/ui/main.py - Batch resize
def _resize_covers(self, files):
    if not self._prepare_batch("Resize Covers Status"):
        return
        
    self.progress_bar.setRange(0, len(files))
    self.progress_bar.setFormat("Resizing... 0%")
    
    self._start_batch_worker(CoverResizeWorker(files))
Resizing is destructive - original cover art is replaced with the resized version. Ensure you have backups if needed.

Loading from File

Manually load cover art from local image files:
1

Click Load Cover

Use the dropdown next to the cover buttons
2

Select Image

Choose a PNG, JPG, JPEG, or BMP file
3

Cover Loaded

Image is displayed in the sidebar
4

Save Changes

Press Ctrl+S to embed in the audio file
# From tagqt/ui/main.py - Load cover from file
def load_cover_from_file(self):
    file_path, _ = QFileDialog.getOpenFileName(
        self, 
        "Select Cover Image", 
        "", 
        "Images (*.png *.jpg *.jpeg *.bmp)"
    )
    if file_path:
        try:
            with open(file_path, 'rb') as f:
                data = f.read()
            
            # Validate image
            pixmap = QPixmap()
            if pixmap.loadFromData(data):
                self.sidebar.set_cover(pixmap)
                if self.metadata:
                    self.metadata.set_cover(data)
            else:
                dialogs.show_warning(self, "Invalid Image", 
                    "The selected file is not a valid image.")
        except Exception as e:
            dialogs.show_error(self, "Error", f"Error loading cover: {e}")

Error Handling

Robust retry logic handles network issues:
# From tagqt/core/art.py - Retry mechanism
def _retry(self, func, max_retries=3):
    for attempt in range(max_retries):
        try:
            return func()
        except (requests.exceptions.RequestException, OSError) as e:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            print(f"Network error after {max_retries} retries: {e}")
            return None
    return None

Best Practices

  • Ensure Artist and Album tags are accurate
  • iTunes typically has better availability
  • MusicBrainz/CAA has higher quality official artwork
  • Try alternative spellings if no results
  • Always save after setting cover art (Ctrl+S)
  • Use 500x500 or smaller for optimal file size
  • JPEG format provides best size/quality balance
  • cover.jpg files are created automatically
  • Use filters to scope operations
  • Review batch results for failures
  • Failed downloads may indicate missing tags
  • Consider resizing to reduce collection size

Tools Menu

  • Covers → Get Covers (All Visible) - Batch download
  • Covers → Resize Covers (Selected) - Resize selected
  • Covers → Resize Covers (All Visible) - Resize all
  • Get Cover - Search and download for current file
  • Load Cover - Load from local file

Context Menu

  • Get Covers (Selected)
  • Resize Covers (Selected)

Next Steps

Batch Operations

Process multiple files efficiently

Lyrics

Manage song lyrics

Build docs developers (and LLMs) love