From c03c8621d7ef5c26b08b1804c1e3246a56050289 Mon Sep 17 00:00:00 2001 From: Milkii Brewster Date: Sun, 16 Nov 2025 02:37:23 +0000 Subject: [PATCH] add catalogue number library column and metadata support implements support for catalogue number metadata field and library column: - adds CatalogueNumber property to AlbumInfo (always enabled, not behind __EXTRA_METADATA__) - adds database schema migration (revision 40) for catalogue_number column - implements tag reading/writing for ID3v2 (TXXX:CATALOGNUMBER), Vorbis (CATALOGNUMBER), MP4 (----:com.apple.iTunes:CATALOGNUMBER), and APEv2 (CatalogNumber) - adds Track getter/setter methods for catalogue number - updates TrackDAO for database persistence (INSERT, UPDATE, SELECT) - adds library column with sorting and display support - column is editable in library view uses existing TagLib wrapper functions and Mixxx metadata infrastructure. follows MusicBrainz Picard tag mapping conventions for catalogue number field. --- res/schema.xml | 8 ++++++++ src/library/basesqltablemodel.cpp | 2 ++ src/library/columncache.cpp | 3 +++ src/library/columncache.h | 1 + src/library/dao/trackdao.cpp | 9 +++++++++ src/library/dao/trackschema.h | 1 + src/library/trackmodel.h | 1 + src/track/albuminfo.cpp | 2 ++ src/track/albuminfo.h | 1 + src/track/taglib/trackmetadata_ape.cpp | 7 +++++++ src/track/taglib/trackmetadata_id3v2.cpp | 12 ++++++++++++ src/track/taglib/trackmetadata_mp4.cpp | 5 +++++ src/track/taglib/trackmetadata_xiph.cpp | 5 +++++ src/track/track.cpp | 13 +++++++++++++ src/track/track.h | 2 ++ 15 files changed, 72 insertions(+) diff --git a/res/schema.xml b/res/schema.xml index d85b4c6ef00b..ac9597fc7c0b 100644 --- a/res/schema.xml +++ b/res/schema.xml @@ -584,4 +584,12 @@ reapplying those migrations. UPDATE library SET filetype='aiff' WHERE filetype='aif'; + + + Add catalogue_number column to library table. + + + ALTER TABLE library ADD COLUMN catalogue_number TEXT DEFAULT ''; + + diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp index c75346dd90a0..a595d8b39243 100644 --- a/src/library/basesqltablemodel.cpp +++ b/src/library/basesqltablemodel.cpp @@ -722,6 +722,8 @@ bool BaseSqlTableModel::setTrackValueForColumn( pTrack->setAlbum(value.toString()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) == column) { pTrack->setAlbumArtist(value.toString()); + } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_CATALOGUENUMBER) == column) { + pTrack->setCatalogueNumber(value.toString()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR) == column) { pTrack->setYear(value.toString()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE) == column) { diff --git a/src/library/columncache.cpp b/src/library/columncache.cpp index 24930d33507f..48ff87b16e24 100644 --- a/src/library/columncache.cpp +++ b/src/library/columncache.cpp @@ -160,6 +160,9 @@ constexpr ColumnProperties kColumnPropertiesByEnum[] = { DI(ColumnCache::COLUMN_LIBRARYTABLE_LAST_PLAYED_AT){&LIBRARYTABLE_LAST_PLAYED_AT, QT_TRANSLATE_NOOP("BaseTrackTableModel", "Last Played"), kDefaultColumnWidth * 3}, + DI(ColumnCache::COLUMN_LIBRARYTABLE_CATALOGUENUMBER){&LIBRARYTABLE_CATALOGUENUMBER, + QT_TRANSLATE_NOOP("BaseTrackTableModel", "Catalogue #"), + kDefaultColumnWidth * 2}, DI(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION){&TRACKLOCATIONSTABLE_LOCATION, QT_TRANSLATE_NOOP("BaseTrackTableModel", "Location"), kDefaultColumnWidth * 6}, diff --git a/src/library/columncache.h b/src/library/columncache.h index 8057107e3376..3f0780375015 100644 --- a/src/library/columncache.h +++ b/src/library/columncache.h @@ -57,6 +57,7 @@ class ColumnCache : public QObject { COLUMN_LIBRARYTABLE_COVERART_DIGEST, COLUMN_LIBRARYTABLE_COVERART_HASH, COLUMN_LIBRARYTABLE_LAST_PLAYED_AT, + COLUMN_LIBRARYTABLE_CATALOGUENUMBER, COLUMN_TRACKLOCATIONSTABLE_LOCATION, COLUMN_TRACKLOCATIONSTABLE_FSDELETED, diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index cae6a5e6d5cb..759358a5b10d 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -442,6 +442,7 @@ void TrackDAO::addTracksPrepare() { "title," "album," "album_artist," + "catalogue_number," "year," "genre," "tracknumber," @@ -490,6 +491,7 @@ void TrackDAO::addTracksPrepare() { ":title," ":album," ":album_artist," + ":catalogue_number," ":year," ":genre," ":tracknumber," @@ -595,6 +597,7 @@ void bindTrackLibraryValues( pTrackLibraryQuery->bindValue(":title", trackInfo.getTitle()); pTrackLibraryQuery->bindValue(":album", albumInfo.getTitle()); pTrackLibraryQuery->bindValue(":album_artist", albumInfo.getArtist()); + pTrackLibraryQuery->bindValue(":catalogue_number", albumInfo.getCatalogueNumber()); pTrackLibraryQuery->bindValue(":year", trackInfo.getYear()); pTrackLibraryQuery->bindValue(":genre", trackInfo.getGenre()); pTrackLibraryQuery->bindValue(":composer", trackInfo.getComposer()); @@ -1170,6 +1173,10 @@ void setTrackAlbumArtist(const QSqlRecord& record, const int column, Track* pTra pTrack->setAlbumArtist(record.value(column).toString()); } +void setTrackCatalogueNumber(const QSqlRecord& record, const int column, Track* pTrack) { + pTrack->setCatalogueNumber(record.value(column).toString()); +} + void setTrackYear(const QSqlRecord& record, const int column, Track* pTrack) { pTrack->setYear(record.value(column).toString()); } @@ -1388,6 +1395,7 @@ TrackPointer TrackDAO::getTrackById(TrackId trackId) const { {"title", setTrackTitle}, {"album", setTrackAlbum}, {"album_artist", setTrackAlbumArtist}, + {"catalogue_number", setTrackCatalogueNumber}, {"year", setTrackYear}, {"genre", setTrackGenre}, {"composer", setTrackComposer}, @@ -1697,6 +1705,7 @@ bool TrackDAO::updateTrack(const Track& track) const { "title=:title," "album=:album," "album_artist=:album_artist," + "catalogue_number=:catalogue_number," "year=:year," "genre=:genre," "composer=:composer," diff --git a/src/library/dao/trackschema.h b/src/library/dao/trackschema.h index dcafdc42c4e1..873ebc603590 100644 --- a/src/library/dao/trackschema.h +++ b/src/library/dao/trackschema.h @@ -15,6 +15,7 @@ const QString LIBRARYTABLE_ARTIST = QStringLiteral("artist"); const QString LIBRARYTABLE_TITLE = QStringLiteral("title"); const QString LIBRARYTABLE_ALBUM = QStringLiteral("album"); const QString LIBRARYTABLE_ALBUMARTIST = QStringLiteral("album_artist"); +const QString LIBRARYTABLE_CATALOGUENUMBER = QStringLiteral("catalogue_number"); const QString LIBRARYTABLE_YEAR = QStringLiteral("year"); const QString LIBRARYTABLE_GENRE = QStringLiteral("genre"); const QString LIBRARYTABLE_COMPOSER = QStringLiteral("composer"); diff --git a/src/library/trackmodel.h b/src/library/trackmodel.h index 551893e2129c..22509e1b528c 100644 --- a/src/library/trackmodel.h +++ b/src/library/trackmodel.h @@ -94,6 +94,7 @@ class TrackModel { Color = 30, LastPlayedAt = 31, PlaylistDateTimeAdded = 32, + CatalogueNumber = 33, // IdMax terminates the list of columns, it must be always after the last item IdMax, diff --git a/src/track/albuminfo.cpp b/src/track/albuminfo.cpp index fdd72f1fc2fb..89372ffe9626 100644 --- a/src/track/albuminfo.cpp +++ b/src/track/albuminfo.cpp @@ -5,6 +5,7 @@ namespace mixxx { bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs) { return (lhs.getArtist() == rhs.getArtist()) && + (lhs.getCatalogueNumber() == rhs.getCatalogueNumber()) && #if defined(__EXTRA_METADATA__) (lhs.getCopyright() == rhs.getCopyright()) && (lhs.getLicense() == rhs.getLicense()) && @@ -20,6 +21,7 @@ bool operator==(const AlbumInfo& lhs, const AlbumInfo& rhs) { QDebug operator<<(QDebug dbg, const AlbumInfo& arg) { dbg << "AlbumInfo{"; arg.dbgArtist(dbg); + arg.dbgCatalogueNumber(dbg); #if defined(__EXTRA_METADATA__) arg.dbgCopyright(dbg); arg.dbgLicense(dbg); diff --git a/src/track/albuminfo.h b/src/track/albuminfo.h index ff1f9cff808a..ccc6e45c59a4 100644 --- a/src/track/albuminfo.h +++ b/src/track/albuminfo.h @@ -13,6 +13,7 @@ namespace mixxx { class AlbumInfo final { // Properties in alphabetical order MIXXX_DECL_PROPERTY(QString, artist, Artist) + MIXXX_DECL_PROPERTY(QString, catalogueNumber, CatalogueNumber) #if defined(__EXTRA_METADATA__) MIXXX_DECL_PROPERTY(QString, copyright, Copyright) MIXXX_DECL_PROPERTY(QString, license, License) diff --git a/src/track/taglib/trackmetadata_ape.cpp b/src/track/taglib/trackmetadata_ape.cpp index b4af3b5afbc6..f0f779515e29 100644 --- a/src/track/taglib/trackmetadata_ape.cpp +++ b/src/track/taglib/trackmetadata_ape.cpp @@ -270,6 +270,12 @@ void importTrackMetadataFromTag( resetMissingTagMetadata) { pTrackMetadata->refAlbumInfo().setRecordLabel(recordLabel); } + QString catalogueNumber; + if (readItem(tag, "CatalogNumber", &catalogueNumber) || + readItem(tag, "CATALOGNUMBER", &catalogueNumber) || + resetMissingTagMetadata) { + pTrackMetadata->refAlbumInfo().setCatalogueNumber(catalogueNumber); + } QString subtitle; if (readItem(tag, "Subtitle", &subtitle) || readItem(tag, "SUBTITLE", &subtitle) || @@ -344,6 +350,7 @@ bool exportTrackMetadataIntoTag(TagLib::APE::Tag* pTag, const TrackMetadata& tra writeItem(pTag, "Copyright", toTString(trackMetadata.getAlbumInfo().getCopyright())); writeItem(pTag, "LICENSE", toTString(trackMetadata.getAlbumInfo().getLicense())); writeItem(pTag, "Label", toTString(trackMetadata.getAlbumInfo().getRecordLabel())); + writeItem(pTag, "CatalogNumber", toTString(trackMetadata.getAlbumInfo().getCatalogueNumber())); writeItem(pTag, "MixArtist", toTString(trackMetadata.getTrackInfo().getRemixer())); writeItem(pTag, "Subtitle", toTString(trackMetadata.getTrackInfo().getSubtitle())); writeItem(pTag, "EncodedBy", toTString(trackMetadata.getTrackInfo().getEncoder())); diff --git a/src/track/taglib/trackmetadata_id3v2.cpp b/src/track/taglib/trackmetadata_id3v2.cpp index 82346781e338..afe4e6b482a3 100644 --- a/src/track/taglib/trackmetadata_id3v2.cpp +++ b/src/track/taglib/trackmetadata_id3v2.cpp @@ -1029,6 +1029,13 @@ void importTrackMetadataFromTag( pTrackMetadata->refAlbumInfo().setRecordLabel( firstNonEmptyFrameToQString(recordLabelFrames)); } + QString catalogueNumber = + readFirstUserTextIdentificationFrame( + tag, + QStringLiteral("CATALOGNUMBER")); + if (!catalogueNumber.isEmpty() || resetMissingTagMetadata) { + pTrackMetadata->refAlbumInfo().setCatalogueNumber(catalogueNumber); + } const TagLib::ID3v2::FrameList remixerFrames(tag.frameListMap()["TPE4"]); if (!remixerFrames.isEmpty() || resetMissingTagMetadata) { pTrackMetadata->refTrackInfo().setRemixer( @@ -1317,6 +1324,11 @@ bool exportTrackMetadataIntoTag(TagLib::ID3v2::Tag* pTag, pTag, "TPUB", trackMetadata.getAlbumInfo().getRecordLabel()); + writeUserTextIdentificationFrame( + pTag, + "CATALOGNUMBER", + trackMetadata.getAlbumInfo().getCatalogueNumber(), + false); writeTextIdentificationFrame( pTag, "TPE4", diff --git a/src/track/taglib/trackmetadata_mp4.cpp b/src/track/taglib/trackmetadata_mp4.cpp index 993d5d0273f3..0c5fb839ad72 100644 --- a/src/track/taglib/trackmetadata_mp4.cpp +++ b/src/track/taglib/trackmetadata_mp4.cpp @@ -327,6 +327,10 @@ void importTrackMetadataFromTag( if (readAtom(tag, "----:com.apple.iTunes:LABEL", &recordLabel) || resetMissingTagMetadata) { pTrackMetadata->refAlbumInfo().setRecordLabel(recordLabel); } + QString catalogueNumber; + if (readAtom(tag, "----:com.apple.iTunes:CATALOGNUMBER", &catalogueNumber) || resetMissingTagMetadata) { + pTrackMetadata->refAlbumInfo().setCatalogueNumber(catalogueNumber); + } QString remixer; if (readAtom(tag, "----:com.apple.iTunes:REMIXER", &remixer) || resetMissingTagMetadata) { pTrackMetadata->refTrackInfo().setRemixer(remixer); @@ -486,6 +490,7 @@ bool exportTrackMetadataIntoTag( writeAtom(pTag, "cprt", toTString(trackMetadata.getAlbumInfo().getCopyright())); writeAtom(pTag, "----:com.apple.iTunes:LICENSE", toTString(trackMetadata.getAlbumInfo().getLicense())); writeAtom(pTag, "----:com.apple.iTunes:LABEL", toTString(trackMetadata.getAlbumInfo().getRecordLabel())); + writeAtom(pTag, "----:com.apple.iTunes:CATALOGNUMBER", toTString(trackMetadata.getAlbumInfo().getCatalogueNumber())); writeAtom(pTag, "----:com.apple.iTunes:REMIXER", toTString(trackMetadata.getTrackInfo().getRemixer())); writeAtom(pTag, "----:com.apple.iTunes:SUBTITLE", toTString(trackMetadata.getTrackInfo().getSubtitle())); writeAtom(pTag, "\251too", toTString(trackMetadata.getTrackInfo().getEncoder())); diff --git a/src/track/taglib/trackmetadata_xiph.cpp b/src/track/taglib/trackmetadata_xiph.cpp index 654de4ad7763..674e82018315 100644 --- a/src/track/taglib/trackmetadata_xiph.cpp +++ b/src/track/taglib/trackmetadata_xiph.cpp @@ -466,6 +466,10 @@ void importTrackMetadataFromTag( if (readCommentField(tag, "LABEL", &recordLabel) || resetMissingTagMetadata) { pTrackMetadata->refAlbumInfo().setRecordLabel(recordLabel); } + QString catalogueNumber; + if (readCommentField(tag, "CATALOGNUMBER", &catalogueNumber) || resetMissingTagMetadata) { + pTrackMetadata->refAlbumInfo().setCatalogueNumber(catalogueNumber); + } QString remixer; if (readCommentField(tag, "REMIXER", &remixer) || resetMissingTagMetadata) { pTrackMetadata->refTrackInfo().setRemixer(remixer); @@ -610,6 +614,7 @@ bool exportTrackMetadataIntoTag( writeCommentField(pTag, "COPYRIGHT", toTString(trackMetadata.getAlbumInfo().getCopyright())); writeCommentField(pTag, "LICENSE", toTString(trackMetadata.getAlbumInfo().getLicense())); writeCommentField(pTag, "LABEL", toTString(trackMetadata.getAlbumInfo().getRecordLabel())); + writeCommentField(pTag, "CATALOGNUMBER", toTString(trackMetadata.getAlbumInfo().getCatalogueNumber())); writeCommentField(pTag, "REMIXER", toTString(trackMetadata.getTrackInfo().getRemixer())); writeCommentField(pTag, "SUBTITLE", toTString(trackMetadata.getTrackInfo().getSubtitle())); writeCommentField(pTag, "ENCODEDBY", toTString(trackMetadata.getTrackInfo().getEncoder())); diff --git a/src/track/track.cpp b/src/track/track.cpp index 8c0a31c9c038..851c2ffad316 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -694,6 +694,19 @@ void Track::setAlbumArtist(const QString& s) { } } +QString Track::getCatalogueNumber() const { + const auto locked = lockMutex(&m_qMutex); + return m_record.getMetadata().getAlbumInfo().getCatalogueNumber(); +} + +void Track::setCatalogueNumber(const QString& s) { + auto locked = lockMutex(&m_qMutex); + const QString value = s.trimmed(); + if (compareAndSet(m_record.refMetadata().refAlbumInfo().ptrCatalogueNumber(), value)) { + markDirtyAndUnlock(&locked); + } +} + QString Track::getYear() const { const auto locked = lockMutex(&m_qMutex); return m_record.getMetadata().getTrackInfo().getYear(); diff --git a/src/track/track.h b/src/track/track.h index 13323bf9e491..7db36014280b 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -182,6 +182,8 @@ class Track : public QObject { void setAlbum(const QString&); QString getAlbumArtist() const; void setAlbumArtist(const QString&); + QString getCatalogueNumber() const; + void setCatalogueNumber(const QString&); // Returns the content of the year library column. // This was original only the four digit (gregorian) calendar year of the release date