Skip to content

Conversation

knguyen1
Copy link
Contributor

@knguyen1 knguyen1 commented Aug 18, 2025

Summary

  • This is a…
    • Bug fix
    • Feature addition
    • Refactoring
    • Minor / simple change (like a typo)
    • Other
  • Describe this change in 1-2 sentences:
    • Introduces a public API to define and register custom columns for File/Album views, including script-based columns, provider protocols, and extensible sorting adapters.

Problem

  • JIRA ticket: PICARD-2103
  • Users cannot add custom/computed columns; limited sorting and no script-backed values in the views.

Solution

  • Added picard.ui.itemviews.custom_columns package:
    • Protocols (ColumnValueProvider, SortKeyProvider), CustomColumn type.
    • Factory helpers: make_field_column, make_script_column, make_callable_column, make_transformed_column.
    • registry to register/unregister columns in File/Album views.
    • Script evaluation (ChainedValueProvider) with caching/perf thresholds and album-loading cache avoidance.
    • Context/resolver layers for fast simple-var lookup and ScriptParser fallback.
    • Sorting adapters: casefold/descending, numeric/descending, article-insensitive, composite, nulls-first/last, length, random, cached, reverse.
  • Comprehensive tests added for API and adapters; type hints and concise NumPy-style docstrings included.

Test With

Append to ./picard/ui/itemviews/columns.py:

# Temporary developer hook to live-test a custom script column.
# Enable by setting environment variable PICARD_DEV_ENABLE_CUSTOM_SCRIPT_COLUMN=1
try:
    import os

    if os.environ.get('PICARD_DEV_ENABLE_CUSTOM_SCRIPT_COLUMN') == '1':  # pragma: no cover - dev only
        from picard.ui.columns import ColumnAlign, ColumnSortType
        from picard.ui.itemviews.custom_columns import (
            CasefoldSortAdapter,
            CustomColumn,
            make_script_column,
            registry,
        )

        # Script-based column
        script = "$if(%title%,$if2(%artist%,Unknown Artist) - $if2(%title%,Unknown Title),$if2(%albumartist%,Unknown Artist) - $if2(%album%,Unknown Album))"
        base_col = make_script_column(
            title="Artist – Title",
            key="artist_title_script",
            script=script,
            width=280,
            align=ColumnAlign.LEFT,
        )

        # Add case-insensitive sorting
        sorted_provider = CasefoldSortAdapter(base_col.provider)
        sorted_col = CustomColumn(
            title=base_col.title,
            key=base_col.key,
            provider=sorted_provider,
            width=base_col.width,
            align=base_col.align,
            sort_type=ColumnSortType.SORTKEY,
        )

        # Register after Title
        registry.register(sorted_col, insert_after_key="title", add_to=["ALBUM_VIEW"])
except Exception:
    # Never break production code due to dev-only snippet
    pass

Then

PICARD_DEV_ENABLE_CUSTOM_SCRIPT_COLUMN=1 python tagger.py

Demo: See Artist - Title custom column

image

Action

Additional actions required:

  • Update Picard documentation (reference this PR)
  • Other:

Note: A follow-up PR is required for custom column management UX (create/edit/delete, ordering, persistence). See: #2714

@knguyen1 knguyen1 force-pushed the feat/PICARD-2103/add-ability-to-add-custom-columns-to-fiewview-and-albumview branch from 4a1744d to b90d846 Compare August 18, 2025 10:29
@knguyen1
Copy link
Contributor Author

Failing lint is from: 03d5c9d
We need a PR to run ruff check --fix and push the changes.

Then I'll rebase knguyen1:feat/PICARD-2103/add-ability-to-add-custom-columns-to-fiewview-and-albumview to master. It'll clear the lint failure.

@phw
Copy link
Member

phw commented Aug 18, 2025

We need a PR to run ruff check --fix and push the changes.

#2710

Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

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

Can you rebase this PR?

@zas zas requested a review from phw August 26, 2025 08:32
@knguyen1 knguyen1 force-pushed the feat/PICARD-2103/add-ability-to-add-custom-columns-to-fiewview-and-albumview branch from 0df8df5 to 1446569 Compare August 26, 2025 15:28
@knguyen1
Copy link
Contributor Author

Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

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

Making columns configurable is a great addition, just few more stylistic comments, but overall it looks good to me.

@zas
Copy link
Collaborator

zas commented Aug 28, 2025

I reverted Ukrainian translation change in master, since it breaks everything, but there's also a problem with tests unrelated to the reverted change. Tests run ok locally though.

Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

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

Patch looks good to me, let's wait for @phw review

@phw
Copy link
Member

phw commented Aug 28, 2025

I don't think it was the translation file per se. Tests passed for that PR: https://github.com/metabrainz/picard/actions/runs/17271183163 . So not sure what changed.

For other failures I suspect there is some issue with some tests not being fully independent. The tests in CI always run randomized. We already had this last week that tests sometimes failed and on retry worked again. We need to investigate and fix the tests.

Copy link
Collaborator

@rdswift rdswift left a comment

Choose a reason for hiding this comment

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

Multiple imports on the same line. I thought our standard was to have each on its own line (using trailing commas).

@rdswift
Copy link
Collaborator

rdswift commented Aug 28, 2025

What happens if multiple custom columns are registered with the same key?

What happens if custom columns are registered with the same key as a standard column (e.g. 'title') that is referenced in another custom column's insert_after_key parameter?

Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

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

Thanks for this. As I said before this is very welcomed addition. And it looks good in general, I have a couple of comments though. We should get this right so we can merge it and use it as the basis to review the other PR with the UI changes.

The code is a bit high on abstraction, but on itself each one makes sense.

@phw
Copy link
Member

phw commented Aug 28, 2025

@knguyen1 Tests on master should now be more stable.

@zas
Copy link
Collaborator

zas commented Aug 31, 2025

I was thinking... what about using this in a more generic manner, that is for ALL columns (that is standard ones)? And drop redundant code.

@knguyen1 @phw What do you think?

@knguyen1
Copy link
Contributor Author

what about using this in a more generic manner, that is for ALL columns (that is standard ones)? And drop redundant code.

That's a good idea; can address it via a new PR after merge. The API will have to be expanded for image columns (MatchQualityColumn) and status icons (Title column)

Converting all text columns to the custom API is straightforward. Sorting parity is achievable with existing adapters. Example conversions:

  • Album (NAT):
make_provider_column(N_("Album"),
   "album",
   FieldReferenceProvider("album"),
   sort_type=ColumnSortType.NAT)
  • Size (numeric sort):
make_provider_column(
       N_("Size"), "~filesize",
       NumericSortAdapter(FieldReferenceProvider("~filesize")),
       align=ColumnAlign.RIGHT
   )

It would help unify/simplify the code quite a bit. e.g. today there is the bespoke sorting key for filesize/bitrate:

def _sortkey_length(obj):
    return obj.metadata.length or 0
# ...
def _sortkey_filesize(obj):
    try:
        return int(obj.metadata['~filesize'] or obj.orig_metadata['~filesize'])
    except ValueError:
        return 0
# ...
def _sortkey_bitrate(obj):
    try:
        return float(obj.metadata['~bitrate'] or obj.orig_metadata['~bitrate'] or 0)
    except (ValueError, TypeError):
        return 0
# ...
def _sortkey_match_quality(obj):
    # percent logic ...

Looks like this: NumericSortAdapter(FieldReferenceProvider("~filesize")), under the new API.

Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

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

LGTM

@zas zas requested a review from phw September 1, 2025 07:55
@zas
Copy link
Collaborator

zas commented Sep 1, 2025

@knguyen1It would be convenient if we could chat about Picard on matrix (see https://musicbrainz.org/doc/Communication/ChatBrainz).
https://matrix.to/#/#musicbrainz-picard-dev:chatbrainz.org is dedicated to Picard
https://matrix.to/#/#metabrainz:chatbrainz.org is where we do MeB meetings
I'm zas there.

Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

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

Thanks for the updates. LGTM

@phw phw merged commit b96252b into metabrainz:master Sep 1, 2025
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants