Skip to content

Add artwork and logo support for additional sources - #329

Open
llttlltt wants to merge 4 commits into
pSpitzner:release/2.0.0from
llttlltt:main
Open

Add artwork and logo support for additional sources#329
llttlltt wants to merge 4 commits into
pSpitzner:release/2.0.0from
llttlltt:main

Conversation

@llttlltt

@llttlltt llttlltt commented Jul 29, 2026

Copy link
Copy Markdown

Hello!

First off I wanted to say thank you for all of your hard work on beets-flask - I’ve been using it for a while now and it’s made managing my library so much quicker and more enjoyable!

I’ve prepared a few improvements to the backend/beets_flask/server/routes/art_preview.py to fetch artwork from Bandcamp, Discogs, and Beatport (implemented as close to the specific beets plugins as possible) - this should resolve #274.

I've also adjusted the cover-art flow so missing artwork behaves reliably and consistently, while hopefully reducing CORS issues by moving more of the external art resolution to the backend (I no longer see CORS errors in Safari so this should fix #275) and added simple-icons (https://simpleicons.org) for source logos in the frontend.

Screenshot - Add artwork and logo support for additional sources

What’s changed

Backend

  • Added backend-proxied external artwork handling in /art.
  • Added support for more providers:
    • Bandcamp
    • Discogs
    • Beatport
  • Updated resolution behavior so unresolved or missing artwork returns a 404 response from backend.

Frontend

  • Updated externalArtQueryOptions to:
    • check /art response status before treating it as success
    • throw HTTPError on non-OK responses
    • return URL.createObjectURL(blob) for successful image payloads

This makes the existing CoverArtError fallback path reliably render when artwork can’t be fetched.

UI

  • Added provider logos via simple-icons in SourceTypeIcon.
  • Added optional useSourceColors?: boolean so icon colouring is opt-in at some point.

Why this helps

  • Missing artwork now consistently triggers frontend error handling and placeholder UI.
  • Artwork fetching is less exposed to browser CORS behavior since it goes through backend proxy flow.
  • The source icon set is more accurate and extensible with consistent provider metadata.

Testing

  • Added/updated integration tests in:
    • backend/tests/integration/test_routes/test_art_preview.py
  • Manual UI verification:
    • missing artwork displays fallback placeholder instead of blank image
    • backend returns 404 for unresolved art URLs
    • error logs are expected and intentional for missing artwork cases

Links / references

Environment

Used for beets-flask in requirements.txt:

beets[discogs]
beetcamp
beets-beatport4

Config for beets-flask in config.yaml:

# ------------------------------------------------------------------------------------ #
#                                   BEETS GUI CONFIG                                   #
# ------------------------------------------------------------------------------------ #
# Example file, this file was automatically copied to /config/beets-flask/config.yaml.
# Feel free to edit this file to customize the gui configuration.
# Especially the `folders` section in the `inbox` section are important to set up your inbox
# folders. You can add as many folders as you like, but don't forget to volume-map them in
# your docker-compose.yml.

gui:
    num_preview_workers: 4 # how many previews to generate in parallel

    library:
        # Use to split artists in the library view if using multiple artists in a field.
        # Set to an empty list to disable this feature.
        artist_separators: [",", ";", "&"]

    terminal:
        start_path: "/music/inbox" # the directory where to start new terminal sessions

    inbox:
        folders:
            # --------------------------------- README -------------------------------- #
            # Before using the inbox feature, you need to create the folders
            # and decide on an inbox type. Have a look at the examples below.

            Inbox1:
                name: "Dummy inbox"
                path: "/music/dummy"
                autotag: no
                # do not automatically trigger tagging and do not automatically import
            Inbox2:
                name: "Auto Inbox"
                path: "/music/inbox_auto"
                autotag: "auto"
                # trigger tag and import if a good match is found based on `auto_threshold`
                auto_threshold: null
                # if set to null, uses the value in beets config (match.strong_rec_thresh)
                # define the distance from a perfect match, i.e. set to 0.1 to import
                # matches with 90% similarity or better.
            Inbox3:
                name: "An Inbox that only generates the previews"
                path: "/music/inbox_preview"
                autotag: "preview"
                # trigger tag but do not import, recommended for most control

Config for beets in config.yaml:

# ------------------------------------------------------------------------------------ #
#                                      BEETS CONFIG                                    #
# ------------------------------------------------------------------------------------ #
# Opinionated example beets configuration. This file was automatically copied to
# /config/beets/config.yaml. Feel free to edit this file to customize the beets
# configuration. For more information on the beets configuration, see
# https://beets.readthedocs.io/en/stable/reference/config.html

plugins: [
        info,
        the,
        fetchart,
        embedart,
        ftintitle,
        lastgenre,
        missing,
        albumtypes,
        scrub,
        zero,
        mbsync,
        duplicates,
        convert,
        fromfilename,
        inline,
        edit,
        # spotify, # needs authentication https://docs.beets.io/en/latest/plugins/spotify.html
        musicbrainz, # needs to be enabled explicitly since beets 2.4.0
        bandcamp,
        discogs,
        beatport4,
    ]

directory: /music/imported
# library: /config/beets/library.db # default location in the container

import:
    move: no
    copy: yes
    write: yes
    log: /music/last_beets_imports.log
    quiet_fallback: skip
    detail: yes
    duplicate_action: ask # ask|skip|merge|keep|remove

ui:
    color: yes

# fix up output file paths
replace:
    '[\\]': ""
    "[_]": "-"
    "[/]": "-"
    '^\.+': ""
    '[\x00-\x1f]': ""
    '[<>:"\?\*\|]': ""
    '\.$': ""
    '\s+$': ""
    '^\s+': ""
    "^-": ""
    "": ""
    "": ""
    "": ""
    "": "-"

per_disc_numbering: no
asciify_paths: yes

# adjusting the `threaded` setting of beets should currently have no effect.
# we launch our own workers for previews and only have one
# import worker that runs one import (file moving) at a time.
threaded: no

fetchart:
    minwidth: 500
    enforce_ratio: 10px # yes|no or tolerance around 1:1 ratio
    sources: coverart filesystem itunes amazon spotify albumart fanarttv

embedart:
    auto: yes
    ifempty: yes # whether to avoid embedding album art for files that already have art embedded.
    remove_art_file: yes

ftintitle:
    auto: yes
    format: (feat. {0})

lastgenre: # get genres from last fm
    auto: yes
    count: 4
    prefer_specific: yes # Sort genres by the most to least specific, rather than most to least popular.
    force: yes # By default, beets will always fetch new genres, even if the files already have one
    source: track # album|track
    separator: "; "
    fallback: ""

match:
    # autotagger tolerance [0, 1], default 0.04
    # for example, 0.1 means 90% similarity required.
    # inbox-folder autotag setting 'auto' respects this and imports above the threshold (like the beets cli would)
    strong_rec_thresh: 0.1

    # customize how penalties affect the match score
    distance_weights:
        # source: 2.0
        # artist: 3.0
        # album: 3.0
        # media: 1.0
        # mediums: 1.0
        # year: 1.0
        # country: 0.5
        # label: 0.5
        # catalognum: 0.5
        # albumdisambig: 0.5
        # album_id: 5.0
        # tracks: 2.0
        data_source: 0.0 # do not apply penalty to any data source plugin
        missing_tracks: 0.2 # If your prefer not being so picky about missing tracks. default 0.9
        # unmatched_tracks: 0.6
        # track_title: 3.0
        # track_artist: 2.0
        # track_index: 1.0
        # track_length: 2.0
        # track_id: 5.0

musicbrainz:
    external_ids:
        discogs: yes
        bandcamp: yes
        spotify: yes
        deezer: yes
        beatport: yes
        tidal: yes

discogs:
    user_token: ""

beatport4:
  username: ""
  password: ""

@github-actions

Copy link
Copy Markdown

Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.

@semohr

semohr commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Hi! Thanks so much for your interest and for taking the time to contribute a PR, we really appreciate it! 🎉

Would you mind rebasing your changes onto our 2.0.0 development branch?

Also, if possible, please try to keep the PR focused on a single change or feature. From our review perspective, several smaller, focused PRs are much easier to review and merge than one large PR with multiple unrelated changes.

Thanks again for your contribution, and we're looking forward to reviewing your PR!


I will have a closer look once you have rebased onto the development branch 🙃

@semohr
semohr changed the base branch from main to release/2.0.0 July 30, 2026 13:12
@llttlltt

Copy link
Copy Markdown
Author

Hello! Thanks for the guidance - I’ve now rebased the PR onto the release/2.0.0 branch and updated the relevant compatibility issues and tests as needed. I’m relatively new to rebasing, so please let me know if anything still needs adjusting.

I’ll be sure to keep future contributions focused on a single feature but hopefully the commits in this PR are self-contained enough to make the changes easier to review.

Thanks again, I appreciate you taking a look 😇

@semohr semohr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of reviews 🙃


images = data.get("images", [])
return images[0].get("uri150") if images else None
except (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All theses functions seem to have similar error handling can we move the error handling into the redirect_external_art function or abstract it?

Returns the url the the art.
"""

get_config()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the config needed here?

if plugin.name != "beatport4":
continue

client = getattr(plugin, "client", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to omit getattr calls. I think an instance check could work instead of relying on name matching.

});
return dataUrl;
);
if (!response.ok) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We overwrite fetch in api/common.ts, We should not need to handle errors here this is done centrally.

SiDiscogs,
SiMusicbrainz,
SiSpotify,
} from '@icons-pack/react-simple-icons';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not too keen on adding another icon library. Any way we can omit this?

I would prefer we just add the brand icons as svgs.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@llttlltt do you know how the simpleicon lib ships the icons?

the motivation of the comment is to keep client side payloads as small as possible.

if only those icons that are imported end up in the compiled site, im in favour of adding the lib.

...props
}: { src: string } & Partial<BoxProps>) {
useEffect(() => {
if (!src.startsWith('blob:')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

@pSpitzner

Copy link
Copy Markdown
Owner

Hey hey,

also thanks from my side and welcome aboard!

Ill drop some more comments - so far nothing critical, just small things to consider.

# Check that url is a valid spotify url
# Check that url is a valid supported source url
redirect_url: str | None = None
if "spotify" in url:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe its time we check for actual url schemas, not just service names - with all the services we now support, we might hit a wrong one if it occurs in the (admittedly unusual) album name.

SiDiscogs,
SiMusicbrainz,
SiSpotify,
} from '@icons-pack/react-simple-icons';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@llttlltt do you know how the simpleicon lib ships the icons?

the motivation of the comment is to keep client side payloads as small as possible.

if only those icons that are imported end up in the compiled site, im in favour of adding the lib.

@llttlltt

llttlltt commented Aug 1, 2026

Copy link
Copy Markdown
Author

Hey both, thanks so much for the detailed feedback and openness to contributions, I'll get around to looking into the comments and addressing them during the week!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants