Git commit 047b22e71d6db70632a66a7719115010b670dbc0 by Urs Fleisch. Committed on 07/01/2026 at 05:12. Pushed by ufleisch into branch 'master'.
Matroska support (#435) BUG 432311 M +56 -50 doc/en/index.docbook M +45 -1 src/core/tags/frame.cpp M +18 -0 src/core/tags/frame.h M +1 -1 src/core/tags/pictureframe.cpp M +77 -0 src/gui/dialogs/editframefieldsdialog.cpp M +5 -0 src/plugins/taglibmetadata/CMakeLists.txt M +6 -0 src/plugins/taglibmetadata/taglibfile.cpp M +6 -0 src/plugins/taglibmetadata/taglibfileiostream.cpp A +802 -0 src/plugins/taglibmetadata/taglibmatroskasupport.cpp [License: GPL (v2+)] A +56 -0 src/plugins/taglibmetadata/taglibmatroskasupport.h [License: GPL (v2+)] https://invent.kde.org/multimedia/kid3/-/commit/047b22e71d6db70632a66a7719115010b670dbc0 diff --git a/doc/en/index.docbook b/doc/en/index.docbook index 6dbadcf5..a89fa38d 100644 --- a/doc/en/index.docbook +++ b/doc/en/index.docbook @@ -784,58 +784,58 @@ format specific frames. <title>Mapping of Unified Frame Types to Various Formats</title> <tgroup cols="7"> <thead> - <row><entry>Unified</entry> <entry>ID3v2.3</entry> <entry>ID3v2.4</entry> <entry>MP4</entry> <entry>ASF</entry> <entry>Vorbis</entry> <entry>RIFF</entry></row> + <row><entry>Unified</entry> <entry>ID3v2.3</entry> <entry>ID3v2.4</entry> <entry>MP4</entry> <entry>ASF</entry> <entry>Vorbis</entry> <entry>RIFF</entry> <entry>Matroska</entry></row> </thead> <tbody> - <row><entry>Title</entry> <entry><literal>TIT2</literal></entry> <entry><literal>TIT2</literal></entry> <entry><literal>©nam</literal></entry> <entry><literal>Title</literal></entry> <entry><literal>TITLE</literal></entry> <entry><literal>INAM</literal></entry></row> - <row><entry>Artist</entry> <entry><literal>TPE1</literal></entry> <entry><literal>TPE1</literal></entry> <entry><literal>©ART</literal></entry> <entry><literal>Author</literal></entry> <entry><literal>ARTIST</literal></entry> <entry><literal>IART</literal></entry></row> - <row><entry>Album</entry> <entry><literal>TALB</literal></entry> <entry><literal>TALB</literal></entry> <entry><literal>©alb</literal></entry> <entry><literal>WM/AlbumTitle</literal></entry> <entry><literal>ALBUM</literal></entry> <entry><literal>IPRD</literal></entry></row> - <row><entry>Comment</entry> <entry><literal>COMM</literal></entry> <entry><literal>COMM</literal></entry> <entry><literal>©cmt</literal></entry> <entry><literal>Description</literal></entry> <entry><literal>COMMENT</literal></entry> <entry><literal>ICMT</literal></entry></row> - <row><entry>Date</entry> <entry><literal>TYER</literal></entry> <entry><literal>TDRC</literal></entry> <entry><literal>©day</literal></entry> <entry><literal>WM/Year</literal></entry> <entry><literal>DATE</literal></entry> <entry><literal>ICRD</literal></entry></row> - <row><entry>Track Number</entry> <entry><literal>TRCK</literal></entry> <entry><literal>TRCK</literal></entry> <entry><literal>trkn</literal></entry> <entry><literal>WM/TrackNumber</literal></entry> <entry><literal>TRACKNUMBER</literal></entry> <entry><literal>IPRT</literal> or <literal>ITRK</literal></entry></row> - <row><entry>Genre</entry> <entry><literal>TCON</literal></entry> <entry><literal>TCON</literal></entry> <entry><literal>©gen</literal></entry> <entry><literal>WM/Genre</literal></entry> <entry><literal>GENRE</literal></entry> <entry><literal>IGNR</literal></entry></row> - <row><entry>Album Artist</entry> <entry><literal>TPE2</literal></entry> <entry><literal>TPE2</literal></entry> <entry><literal>aART</literal></entry> <entry><literal>WM/AlbumArtist</literal></entry> <entry><literal>ALBUMARTIST</literal></entry> <entry></entry></row> - <row><entry>Arranger</entry> <entry><literal>IPLS</literal></entry> <entry><literal>TIPL</literal></entry> <entry><literal>ARRANGER</literal></entry> <entry><literal>WM/Producer</literal></entry> <entry><literal>ARRANGER</literal></entry> <entry><literal>IENG</literal></entry></row> - <row><entry>Author</entry> <entry><literal>TOLY</literal></entry> <entry><literal>TOLY</literal></entry> <entry><literal>AUTHOR</literal></entry> <entry></entry> <entry><literal>AUTHOR</literal></entry> <entry></entry></row> - <row><entry>BPM</entry> <entry><literal>TBPM</literal></entry> <entry><literal>TBPM</literal></entry> <entry><literal>tmpo</literal></entry> <entry><literal>WM/BeatsPerMinute</literal></entry> <entry><literal>BPM</literal></entry> <entry><literal>IBPM</literal></entry></row> - <row><entry>Catalog Number</entry> <entry><literal>TXXX:CATALOGNUMBER</literal></entry> <entry><literal>TXXX:CATALOGNUMBER</literal></entry> <entry></entry> <entry></entry> <entry><literal>CATALOGNUMBER</literal></entry> <entry></entry></row> - <row><entry>Compilation</entry> <entry><literal>TCMP</literal></entry> <entry><literal>TCMP</literal></entry> <entry><literal>cpil</literal></entry> <entry></entry> <entry><literal>COMPILATION</literal></entry> <entry></entry></row> - <row><entry>Composer</entry> <entry><literal>TCOM</literal></entry> <entry><literal>TCOM</literal></entry> <entry><literal>©wrt</literal></entry> <entry><literal>WM/Composer</literal></entry> <entry><literal>COMPOSER</literal></entry> <entry><literal>IMUS</literal></entry></row> - <row><entry>Conductor</entry> <entry><literal>TPE3</literal></entry> <entry><literal>TPE3</literal></entry> <entry><literal>CONDUCTOR</literal></entry> <entry><literal>WM/Conductor</literal></entry> <entry><literal>CONDUCTOR</literal></entry> <entry></entry></row> - <row><entry>Copyright</entry> <entry><literal>TCOP</literal></entry> <entry><literal>TCOP</literal></entry> <entry><literal>cprt</literal></entry> <entry><literal>Copyright</literal></entry> <entry><literal>COPYRIGHT</literal></entry> <entry><literal>ICOP</literal></entry></row> - <row><entry>Description</entry> <entry><literal>TIT3</literal></entry> <entry><literal>TIT3</literal></entry> <entry><literal>desc</literal></entry> <entry><literal>WM/SubTitleDescription</literal></entry> <entry><literal>DESCRIPTION</literal></entry> <entry></entry></row> - <row><entry>Disc Number</entry> <entry><literal>TPOS</literal></entry> <entry><literal>TPOS</literal></entry> <entry><literal>disk</literal></entry> <entry><literal>WM/PartOfSet</literal></entry> <entry><literal>DISCNUMBER</literal></entry> <entry></entry></row> - <row><entry>Encoded-by</entry> <entry><literal>TENC</literal></entry> <entry><literal>TENC</literal></entry> <entry><literal>©enc</literal></entry> <entry><literal>WM/EncodedBy</literal></entry> <entry><literal>ENCODED-BY</literal></entry> <entry><literal>ITCH</literal></entry></row> - <row><entry>Encoder Settings</entry> <entry><literal>TSSE</literal></entry> <entry><literal>TSSE</literal></entry> <entry><literal>©too</literal></entry> <entry><literal>WM/EncodingSettings</literal></entry> <entry><literal>ENCODERSETTINGS</literal></entry> <entry><literal>ISFT</literal></entry></row> - <row><entry>Encoding Time</entry> <entry></entry> <entry><literal>TDEN</literal></entry> <entry></entry> <entry><literal>WM/EncodingTime</literal></entry> <entry><literal>ENCODINGTIME</literal></entry> <entry><literal>IDIT</literal></entry></row> - <row><entry>Grouping</entry> <entry><literal>GRP1</literal></entry> <entry><literal>GRP1</literal></entry> <entry><literal>©grp</literal></entry> <entry></entry> <entry><literal>GROUPING</literal></entry> <entry></entry></row> - <row><entry>Initial Key</entry> <entry><literal>TKEY</literal></entry> <entry><literal>TKEY</literal></entry> <entry></entry> <entry><literal>WM/InitialKey</literal></entry> <entry><literal>INITIALKEY</literal></entry> <entry></entry></row> - <row><entry>ISRC</entry> <entry><literal>TSRC</literal></entry> <entry><literal>TSRC</literal></entry> <entry><literal>ISRC</literal></entry> <entry><literal>WM/ISRC</literal></entry> <entry><literal>ISRC</literal></entry> <entry><literal>ISRC</literal></entry></row> - <row><entry>Language</entry> <entry><literal>TLAN</literal></entry> <entry><literal>TLAN</literal></entry> <entry><literal>LANGUAGE</literal></entry> <entry><literal>WM/Language</literal></entry> <entry><literal>LANGUAGE</literal></entry> <entry><literal>ILNG</literal></entry></row> - <row><entry>Lyricist</entry> <entry><literal>TEXT</literal></entry> <entry><literal>TEXT</literal></entry> <entry><literal>LYRICIST</literal></entry> <entry><literal>WM/Writer</literal></entry> <entry><literal>LYRICIST</literal></entry> <entry><literal>IWRI</literal></entry></row> - <row><entry>Lyrics</entry> <entry><literal>USLT</literal></entry> <entry><literal>USLT</literal></entry> <entry><literal>©lyr</literal></entry> <entry><literal>WM/Lyrics</literal></entry> <entry><literal>LYRICS</literal></entry> <entry></entry></row> - <row><entry>Media</entry> <entry><literal>TMED</literal></entry> <entry><literal>TMED</literal></entry> <entry><literal>SOURCEMEDIA</literal></entry> <entry></entry> <entry><literal>SOURCEMEDIA</literal></entry> <entry><literal>IMED</literal></entry></row> - <row><entry>Mood</entry> <entry></entry> <entry><literal>TMOO</literal></entry> <entry></entry> <entry><literal>WM/Mood</literal></entry> <entry><literal>MOOD</literal></entry> <entry></entry></row> - <row><entry>Original Album</entry> <entry><literal>TOAL</literal></entry> <entry><literal>TOAL</literal></entry> <entry><literal>ORIGINALALBUM</literal></entry> <entry><literal>WM/OriginalAlbumTitle</literal></entry> <entry><literal>ORIGINALALBUM</literal></entry> <entry></entry></row> - <row><entry>Original Artist</entry> <entry><literal>TOPE</literal></entry> <entry><literal>TOPE</literal></entry> <entry><literal>ORIGINALARTIST</literal></entry> <entry><literal>WM/OriginalArtist</literal></entry> <entry><literal>ORIGINALARTIST</literal></entry> <entry></entry></row> - <row><entry>Original Date</entry> <entry><literal>TORY</literal></entry> <entry><literal>TDOR</literal></entry> <entry><literal>ORIGINALDATE</literal></entry> <entry><literal>WM/OriginalReleaseYear</literal></entry> <entry><literal>ORIGINALDATE</literal></entry> <entry></entry></row> - <row><entry>Performer</entry> <entry><literal>IPLS</literal></entry> <entry><literal>TMCL</literal></entry> <entry><literal>PERFORMER</literal></entry> <entry></entry> <entry><literal>PERFORMER</literal></entry> <entry><literal>ISTR</literal></entry></row> - <row><entry>Picture</entry> <entry><literal>APIC</literal></entry> <entry><literal>APIC</literal></entry> <entry><literal>covr</literal></entry> <entry><literal>WM/Picture</literal></entry> <entry><literal>METADATA_BLOCK_PICTURE</literal></entry> <entry></entry></row> - <row><entry>Publisher</entry> <entry><literal>TPUB</literal></entry> <entry><literal>TPUB</literal></entry> <entry><literal>PUBLISHER</literal></entry> <entry><literal>WM/Publisher</literal></entry> <entry><literal>PUBLISHER</literal></entry> <entry><literal>IPUB</literal></entry></row> - <row><entry>Rating</entry> <entry><literal>POPM</literal></entry> <entry><literal>POPM</literal></entry> <entry><literal>rate</literal></entry> <entry><literal>WM/SharedUserRating</literal></entry> <entry><literal>RATING</literal></entry> <entry><literal>IRTD</literal></entry></row> - <row><entry>Release Country</entry> <entry><literal>TXXX:RELEASECOUNTRY</literal></entry> <entry><literal>TXXX:RELEASECOUNTRY</literal></entry> <entry></entry> <entry></entry> <entry><literal>RELEASECOUNTRY</literal></entry> <entry><literal>ICNT</literal></entry></row> - <row><entry>Release Date</entry> <entry></entry> <entry><literal>TDRL</literal></entry> <entry><literal>RELEASEDATE</literal></entry> <entry></entry> <entry><literal>RELEASEDATE</literal></entry> <entry></entry></row> - <row><entry>Remixer</entry> <entry><literal>TPE4</literal></entry> <entry><literal>TPE4</literal></entry> <entry><literal>REMIXER</literal></entry> <entry><literal>WM/ModifiedBy</literal></entry> <entry><literal>REMIXER</literal></entry> <entry><literal>IEDT</literal></entry></row> - <row><entry>Sort Album</entry> <entry><literal>TSOA</literal></entry> <entry><literal>TSOA</literal></entry> <entry><literal>soal</literal></entry> <entry><literal>WM/AlbumSortOrder</literal></entry> <entry><literal>ALBUMSORT</literal></entry> <entry></entry></row> - <row><entry>Sort Album Artist</entry> <entry><literal>TSO2</literal></entry> <entry><literal>TSO2</literal></entry> <entry><literal>soaa</literal></entry> <entry></entry> <entry><literal>ALBUMARTISTSORT</literal></entry> <entry></entry></row> - <row><entry>Sort Artist</entry> <entry><literal>TSOP</literal></entry> <entry><literal>TSOP</literal></entry> <entry><literal>soar</literal></entry> <entry><literal>WM/ArtistSortOrder</literal></entry> <entry><literal>ARTISTSORT</literal></entry> <entry></entry></row> - <row><entry>Sort Composer</entry> <entry><literal>TSOC</literal></entry> <entry><literal>TSOC</literal></entry> <entry><literal>soco</literal></entry> <entry></entry> <entry><literal>COMPOSERSORT</literal></entry> <entry></entry></row> - <row><entry>Sort Name</entry> <entry><literal>TSOT</literal></entry> <entry><literal>TSOT</literal></entry> <entry><literal>sonm</literal></entry> <entry><literal>WM/TitleSortOrder</literal></entry> <entry><literal>TITLESORT</literal></entry> <entry></entry></row> - <row><entry>Subtitle</entry> <entry></entry> <entry><literal>TSST</literal></entry> <entry><literal>SUBTITLE</literal></entry> <entry><literal>WM/SubTitle</literal></entry> <entry><literal>SUBTITLE</literal></entry> <entry><literal>PRT1</literal></entry></row> - <row><entry>Website</entry> <entry><literal>WOAR</literal></entry> <entry><literal>WOAR</literal></entry> <entry><literal>WEBSITE</literal></entry> <entry><literal>WM/AuthorURL</literal></entry> <entry><literal>WEBSITE</literal></entry> <entry><literal>IBSU</literal></entry></row> - <row><entry>Work</entry> <entry><literal>TIT1</literal></entry> <entry><literal>TIT1</literal></entry> <entry><literal>©wrk</literal></entry> <entry><literal>WM/ContentGroupDescription</literal></entry> <entry><literal>WORK</literal></entry> <entry></entry></row> - <row><entry>WWW Audio File</entry> <entry><literal>WOAF</literal></entry> <entry><literal>WOAF</literal></entry> <entry></entry> <entry><literal>WM/AudioFileURL</literal></entry> <entry><literal>WWWAUDIOFILE</literal></entry> <entry></entry></row> - <row><entry>WWW Audio Source</entry> <entry><literal>WOAS</literal></entry> <entry><literal>WOAS</literal></entry> <entry></entry> <entry><literal>WM/AudioSourceURL</literal></entry> <entry><literal>WWWAUDIOSOURCE</literal></entry> <entry></entry></row> + <row><entry>Title</entry> <entry><literal>TIT2</literal></entry> <entry><literal>TIT2</literal></entry> <entry><literal>©nam</literal></entry> <entry><literal>Title</literal></entry> <entry><literal>TITLE</literal></entry> <entry><literal>INAM</literal></entry> <entry><literal>TITLE</literal></entry></row> + <row><entry>Artist</entry> <entry><literal>TPE1</literal></entry> <entry><literal>TPE1</literal></entry> <entry><literal>©ART</literal></entry> <entry><literal>Author</literal></entry> <entry><literal>ARTIST</literal></entry> <entry><literal>IART</literal></entry> <entry><literal>ARTIST</literal></entry></row> + <row><entry>Album</entry> <entry><literal>TALB</literal></entry> <entry><literal>TALB</literal></entry> <entry><literal>©alb</literal></entry> <entry><literal>WM/AlbumTitle</literal></entry> <entry><literal>ALBUM</literal></entry> <entry><literal>IPRD</literal></entry> <entry><literal>TITLE/50</literal></entry></row> + <row><entry>Comment</entry> <entry><literal>COMM</literal></entry> <entry><literal>COMM</literal></entry> <entry><literal>©cmt</literal></entry> <entry><literal>Description</literal></entry> <entry><literal>COMMENT</literal></entry> <entry><literal>ICMT</literal></entry> <entry><literal>COMMENT</literal></entry></row> + <row><entry>Date</entry> <entry><literal>TYER</literal></entry> <entry><literal>TDRC</literal></entry> <entry><literal>©day</literal></entry> <entry><literal>WM/Year</literal></entry> <entry><literal>DATE</literal></entry> <entry><literal>ICRD</literal></entry> <entry><literal>DATE_RECORDED</literal></entry></row> + <row><entry>Track Number</entry> <entry><literal>TRCK</literal></entry> <entry><literal>TRCK</literal></entry> <entry><literal>trkn</literal></entry> <entry><literal>WM/TrackNumber</literal></entry> <entry><literal>TRACKNUMBER</literal></entry> <entry><literal>IPRT</literal> or <literal>ITRK</literal></entry> <entry><literal>PART_NUMBER</literal></entry></row> + <row><entry>Genre</entry> <entry><literal>TCON</literal></entry> <entry><literal>TCON</literal></entry> <entry><literal>©gen</literal></entry> <entry><literal>WM/Genre</literal></entry> <entry><literal>GENRE</literal></entry> <entry><literal>IGNR</literal></entry> <entry><literal>GENRE</literal></entry></row> + <row><entry>Album Artist</entry> <entry><literal>TPE2</literal></entry> <entry><literal>TPE2</literal></entry> <entry><literal>aART</literal></entry> <entry><literal>WM/AlbumArtist</literal></entry> <entry><literal>ALBUMARTIST</literal></entry> <entry></entry> <entry><literal>ARTIST/50</literal></entry></row> + <row><entry>Arranger</entry> <entry><literal>IPLS</literal></entry> <entry><literal>TIPL</literal></entry> <entry><literal>ARRANGER</literal></entry> <entry><literal>WM/Producer</literal></entry> <entry><literal>ARRANGER</literal></entry> <entry><literal>IENG</literal></entry> <entry><literal>ARRANGER</literal></entry></row> + <row><entry>Author</entry> <entry><literal>TOLY</literal></entry> <entry><literal>TOLY</literal></entry> <entry><literal>AUTHOR</literal></entry> <entry></entry> <entry><literal>AUTHOR</literal></entry> <entry></entry> <entry><literal>WRITTEN_BY</literal></entry></row> + <row><entry>BPM</entry> <entry><literal>TBPM</literal></entry> <entry><literal>TBPM</literal></entry> <entry><literal>tmpo</literal></entry> <entry><literal>WM/BeatsPerMinute</literal></entry> <entry><literal>BPM</literal></entry> <entry><literal>IBPM</literal></entry> <entry><literal>BPM</literal></entry></row> + <row><entry>Catalog Number</entry> <entry><literal>TXXX:CATALOGNUMBER</literal></entry> <entry><literal>TXXX:CATALOGNUMBER</literal></entry> <entry></entry> <entry></entry> <entry><literal>CATALOGNUMBER</literal></entry> <entry></entry> <entry><literal>CATALOG_NUMBER</literal></entry></row> + <row><entry>Compilation</entry> <entry><literal>TCMP</literal></entry> <entry><literal>TCMP</literal></entry> <entry><literal>cpil</literal></entry> <entry></entry> <entry><literal>COMPILATION</literal></entry> <entry></entry> <entry><literal>COMPILATION</literal></entry></row> + <row><entry>Composer</entry> <entry><literal>TCOM</literal></entry> <entry><literal>TCOM</literal></entry> <entry><literal>©wrt</literal></entry> <entry><literal>WM/Composer</literal></entry> <entry><literal>COMPOSER</literal></entry> <entry><literal>IMUS</literal></entry> <entry><literal>COMPOSER</literal></entry></row> + <row><entry>Conductor</entry> <entry><literal>TPE3</literal></entry> <entry><literal>TPE3</literal></entry> <entry><literal>CONDUCTOR</literal></entry> <entry><literal>WM/Conductor</literal></entry> <entry><literal>CONDUCTOR</literal></entry> <entry></entry> <entry><literal>CONDUCTOR</literal></entry></row> + <row><entry>Copyright</entry> <entry><literal>TCOP</literal></entry> <entry><literal>TCOP</literal></entry> <entry><literal>cprt</literal></entry> <entry><literal>Copyright</literal></entry> <entry><literal>COPYRIGHT</literal></entry> <entry><literal>ICOP</literal></entry> <entry><literal>COPYRIGHT</literal></entry></row> + <row><entry>Description</entry> <entry><literal>TIT3</literal></entry> <entry><literal>TIT3</literal></entry> <entry><literal>desc</literal></entry> <entry><literal>WM/SubTitleDescription</literal></entry> <entry><literal>DESCRIPTION</literal></entry> <entry></entry> <entry><literal>DESCRIPTION</literal></entry></row> + <row><entry>Disc Number</entry> <entry><literal>TPOS</literal></entry> <entry><literal>TPOS</literal></entry> <entry><literal>disk</literal></entry> <entry><literal>WM/PartOfSet</literal></entry> <entry><literal>DISCNUMBER</literal></entry> <entry></entry> <entry><literal>PART_NUMBER/50</literal></entry></row> + <row><entry>Encoded-by</entry> <entry><literal>TENC</literal></entry> <entry><literal>TENC</literal></entry> <entry><literal>©enc</literal></entry> <entry><literal>WM/EncodedBy</literal></entry> <entry><literal>ENCODED-BY</literal></entry> <entry><literal>ITCH</literal></entry> <entry><literal>ENCODER</literal></entry></row> + <row><entry>Encoder Settings</entry> <entry><literal>TSSE</literal></entry> <entry><literal>TSSE</literal></entry> <entry><literal>©too</literal></entry> <entry><literal>WM/EncodingSettings</literal></entry> <entry><literal>ENCODERSETTINGS</literal></entry> <entry><literal>ISFT</literal></entry> <entry><literal>ENCODER_SETTINGS</literal></entry></row> + <row><entry>Encoding Time</entry> <entry></entry> <entry><literal>TDEN</literal></entry> <entry></entry> <entry><literal>WM/EncodingTime</literal></entry> <entry><literal>ENCODINGTIME</literal></entry> <entry><literal>IDIT</literal></entry> <entry><literal>DATE_ENCODED</literal></entry></row> + <row><entry>Grouping</entry> <entry><literal>GRP1</literal></entry> <entry><literal>GRP1</literal></entry> <entry><literal>©grp</literal></entry> <entry></entry> <entry><literal>GROUPING</literal></entry> <entry></entry> <entry><literal>GROUPING</literal></entry></row> + <row><entry>Initial Key</entry> <entry><literal>TKEY</literal></entry> <entry><literal>TKEY</literal></entry> <entry></entry> <entry><literal>WM/InitialKey</literal></entry> <entry><literal>INITIALKEY</literal></entry> <entry></entry> <entry><literal>INITIAL_KEY</literal></entry></row> + <row><entry>ISRC</entry> <entry><literal>TSRC</literal></entry> <entry><literal>TSRC</literal></entry> <entry><literal>ISRC</literal></entry> <entry><literal>WM/ISRC</literal></entry> <entry><literal>ISRC</literal></entry> <entry><literal>ISRC</literal></entry> <entry><literal>ISRC</literal></entry></row> + <row><entry>Language</entry> <entry><literal>TLAN</literal></entry> <entry><literal>TLAN</literal></entry> <entry><literal>LANGUAGE</literal></entry> <entry><literal>WM/Language</literal></entry> <entry><literal>LANGUAGE</literal></entry> <entry><literal>ILNG</literal></entry> <entry><literal>LANGUAGE</literal></entry></row> + <row><entry>Lyricist</entry> <entry><literal>TEXT</literal></entry> <entry><literal>TEXT</literal></entry> <entry><literal>LYRICIST</literal></entry> <entry><literal>WM/Writer</literal></entry> <entry><literal>LYRICIST</literal></entry> <entry><literal>IWRI</literal></entry> <entry><literal>LYRICIST</literal></entry></row> + <row><entry>Lyrics</entry> <entry><literal>USLT</literal></entry> <entry><literal>USLT</literal></entry> <entry><literal>©lyr</literal></entry> <entry><literal>WM/Lyrics</literal></entry> <entry><literal>LYRICS</literal></entry> <entry></entry> <entry><literal>LYRICS</literal></entry></row> + <row><entry>Media</entry> <entry><literal>TMED</literal></entry> <entry><literal>TMED</literal></entry> <entry><literal>SOURCEMEDIA</literal></entry> <entry></entry> <entry><literal>SOURCEMEDIA</literal></entry> <entry><literal>IMED</literal></entry> <entry><literal>ORIGINAL_MEDIA_TYPE</literal></entry></row> + <row><entry>Mood</entry> <entry></entry> <entry><literal>TMOO</literal></entry> <entry></entry> <entry><literal>WM/Mood</literal></entry> <entry><literal>MOOD</literal></entry> <entry></entry> <entry><literal>MOOD</literal></entry></row> + <row><entry>Original Album</entry> <entry><literal>TOAL</literal></entry> <entry><literal>TOAL</literal></entry> <entry><literal>ORIGINALALBUM</literal></entry> <entry><literal>WM/OriginalAlbumTitle</literal></entry> <entry><literal>ORIGINALALBUM</literal></entry> <entry></entry> <entry><literal>ORIGINALALBUM</literal></entry></row> + <row><entry>Original Artist</entry> <entry><literal>TOPE</literal></entry> <entry><literal>TOPE</literal></entry> <entry><literal>ORIGINALARTIST</literal></entry> <entry><literal>WM/OriginalArtist</literal></entry> <entry><literal>ORIGINALARTIST</literal></entry> <entry></entry> <entry><literal>ORIGINALARTIST</literal></entry></row> + <row><entry>Original Date</entry> <entry><literal>TORY</literal></entry> <entry><literal>TDOR</literal></entry> <entry><literal>ORIGINALDATE</literal></entry> <entry><literal>WM/OriginalReleaseYear</literal></entry> <entry><literal>ORIGINALDATE</literal></entry> <entry></entry> <entry><literal>ORIGINALDATE</literal></entry></row> + <row><entry>Performer</entry> <entry><literal>IPLS</literal></entry> <entry><literal>TMCL</literal></entry> <entry><literal>PERFORMER</literal></entry> <entry></entry> <entry><literal>PERFORMER</literal></entry> <entry><literal>ISTR</literal></entry> <entry><literal>PERFORMER</literal></entry></row> + <row><entry>Picture</entry> <entry><literal>APIC</literal></entry> <entry><literal>APIC</literal></entry> <entry><literal>covr</literal></entry> <entry><literal>WM/Picture</literal></entry> <entry><literal>METADATA_BLOCK_PICTURE</literal></entry> <entry></entry> <entry>Attachment</entry></row> + <row><entry>Publisher</entry> <entry><literal>TPUB</literal></entry> <entry><literal>TPUB</literal></entry> <entry><literal>PUBLISHER</literal></entry> <entry><literal>WM/Publisher</literal></entry> <entry><literal>PUBLISHER</literal></entry> <entry><literal>IPUB</literal></entry> <entry><literal>LABEL_CODE</literal></entry></row> + <row><entry>Rating</entry> <entry><literal>POPM</literal></entry> <entry><literal>POPM</literal></entry> <entry><literal>rate</literal></entry> <entry><literal>WM/SharedUserRating</literal></entry> <entry><literal>RATING</literal></entry> <entry><literal>IRTD</literal></entry> <entry><literal>RATING</literal></entry></row> + <row><entry>Release Country</entry> <entry><literal>TXXX:RELEASECOUNTRY</literal></entry> <entry><literal>TXXX:RELEASECOUNTRY</literal></entry> <entry></entry> <entry></entry> <entry><literal>RELEASECOUNTRY</literal></entry> <entry><literal>ICNT</literal></entry> <entry><literal>RELEASECOUNTRY</literal></entry></row> + <row><entry>Release Date</entry> <entry></entry> <entry><literal>TDRL</literal></entry> <entry><literal>RELEASEDATE</literal></entry> <entry></entry> <entry><literal>RELEASEDATE</literal></entry> <entry></entry> <entry><literal>DATE_RELEASED/50</literal></entry></row> + <row><entry>Remixer</entry> <entry><literal>TPE4</literal></entry> <entry><literal>TPE4</literal></entry> <entry><literal>REMIXER</literal></entry> <entry><literal>WM/ModifiedBy</literal></entry> <entry><literal>REMIXER</literal></entry> <entry><literal>IEDT</literal></entry> <entry><literal>REMIXED_BY</literal></entry></row> + <row><entry>Sort Album</entry> <entry><literal>TSOA</literal></entry> <entry><literal>TSOA</literal></entry> <entry><literal>soal</literal></entry> <entry><literal>WM/AlbumSortOrder</literal></entry> <entry><literal>ALBUMSORT</literal></entry> <entry></entry> <entry><literal>TITLESORT/50</literal></entry></row> + <row><entry>Sort Album Artist</entry> <entry><literal>TSO2</literal></entry> <entry><literal>TSO2</literal></entry> <entry><literal>soaa</literal></entry> <entry></entry> <entry><literal>ALBUMARTISTSORT</literal></entry> <entry></entry> <entry><literal>ARTISTSORT/50</literal></entry></row> + <row><entry>Sort Artist</entry> <entry><literal>TSOP</literal></entry> <entry><literal>TSOP</literal></entry> <entry><literal>soar</literal></entry> <entry><literal>WM/ArtistSortOrder</literal></entry> <entry><literal>ARTISTSORT</literal></entry> <entry></entry> <entry><literal>ARTISTSORT</literal></entry></row> + <row><entry>Sort Composer</entry> <entry><literal>TSOC</literal></entry> <entry><literal>TSOC</literal></entry> <entry><literal>soco</literal></entry> <entry></entry> <entry><literal>COMPOSERSORT</literal></entry> <entry></entry> <entry><literal>COMPOSERSORT</literal></entry></row> + <row><entry>Sort Name</entry> <entry><literal>TSOT</literal></entry> <entry><literal>TSOT</literal></entry> <entry><literal>sonm</literal></entry> <entry><literal>WM/TitleSortOrder</literal></entry> <entry><literal>TITLESORT</literal></entry> <entry></entry> <entry><literal>TITLESORT</literal></entry></row> + <row><entry>Subtitle</entry> <entry></entry> <entry><literal>TSST</literal></entry> <entry><literal>SUBTITLE</literal></entry> <entry><literal>WM/SubTitle</literal></entry> <entry><literal>SUBTITLE</literal></entry> <entry><literal>PRT1</literal></entry> <entry><literal>SUBTITLE</literal></entry></row> + <row><entry>Website</entry> <entry><literal>WOAR</literal></entry> <entry><literal>WOAR</literal></entry> <entry><literal>WEBSITE</literal></entry> <entry><literal>WM/AuthorURL</literal></entry> <entry><literal>WEBSITE</literal></entry> <entry><literal>IBSU</literal></entry> <entry><literal>WEBSITE</literal></entry></row> + <row><entry>Work</entry> <entry><literal>TIT1</literal></entry> <entry><literal>TIT1</literal></entry> <entry><literal>©wrk</literal></entry> <entry><literal>WM/ContentGroupDescription</literal></entry> <entry><literal>WORK</literal></entry> <entry></entry> <entry><literal>WORK</literal></entry></row> + <row><entry>WWW Audio File</entry> <entry><literal>WOAF</literal></entry> <entry><literal>WOAF</literal></entry> <entry></entry> <entry><literal>WM/AudioFileURL</literal></entry> <entry><literal>WWWAUDIOFILE</literal></entry> <entry></entry> <entry><literal>WWWAUDIOFILE</literal></entry></row> + <row><entry>WWW Audio Source</entry> <entry><literal>WOAS</literal></entry> <entry><literal>WOAS</literal></entry> <entry></entry> <entry><literal>WM/AudioSourceURL</literal></entry> <entry><literal>WWWAUDIOSOURCE</literal></entry> <entry></entry> <entry><literal>WWWAUDIOSOURCE</literal></entry></row> </tbody> </tgroup> </table> @@ -874,6 +874,12 @@ Values in this format are also set when importing data from servers which offer this information. </para></listitem> <listitem><para> +For Matroska the target level is listed, e.g. /50 for Album, unless it is the +default of /30 for Track. If you want to create a Matroska simple tag with +binary content, append " - binary" when adding the frame. Note that such tags +are rarely used, cover art is stored as an attached file. +</para></listitem> +<listitem><para> To explicitly use a specific frame name which conflicts with a unified frame name, prepend an exclamation mark. For example, adding a frame of type "<replaceable>Media</replaceable>" to a Vorbis comment will create a frame with diff --git a/src/core/tags/frame.cpp b/src/core/tags/frame.cpp index dfbd5119..d9c1388c 100644 --- a/src/core/tags/frame.cpp +++ b/src/core/tags/frame.cpp @@ -70,6 +70,9 @@ const char* const fieldIdNames[] = { QT_TRANSLATE_NOOP("@default", "Price"), QT_TRANSLATE_NOOP("@default", "Date"), QT_TRANSLATE_NOOP("@default", "Seller"), + + QT_TRANSLATE_NOOP("@default", "Target Type"), + QT_TRANSLATE_NOOP("@default", "Default"), nullptr }; @@ -99,6 +102,18 @@ const char* const contentTypeNames[] = { nullptr }; +const char* const targetTypeNames[] = { + QT_TRANSLATE_NOOP("@default", "None"), + QT_TRANSLATE_NOOP("@default", "Shot"), + QT_TRANSLATE_NOOP("@default", "Subtrack"), + QT_TRANSLATE_NOOP("@default", "Track"), + QT_TRANSLATE_NOOP("@default", "Part"), + QT_TRANSLATE_NOOP("@default", "Album"), + QT_TRANSLATE_NOOP("@default", "Edition"), + QT_TRANSLATE_NOOP("@default", "Collection"), + nullptr +}; + // Custom frame names. QVector<QByteArray> customFrameNames(Frame::NUM_CUSTOM_FRAME_NAMES); @@ -280,6 +295,11 @@ QMap<QByteArray, QByteArray> getDisplayNamesOfIds() { "VERSION", QT_TRANSLATE_NOOP("@default", "Version") }, { "VOLUME", QT_TRANSLATE_NOOP("@default", "Volume") }, { "WWW", QT_TRANSLATE_NOOP("@default", "User-defined URL") }, + { "DIRECTOR", QT_TRANSLATE_NOOP("@default", "Director") }, + { "DURATION", QT_TRANSLATE_NOOP("@default", "Duration") }, + { "SUMMARY", QT_TRANSLATE_NOOP("@default", "Summary") }, + { "SYNOPSIS", QT_TRANSLATE_NOOP("@default", "Synopsis") }, + { "TOTAL_PARTS", QT_TRANSLATE_NOOP("@default", "Total Parts") }, { "WM/AlbumArtistSortOrder", QT_TRANSLATE_NOOP("@default", "Sort Album Artist") }, { "WM/Comments", QT_TRANSLATE_NOOP("@default", "Comment") }, { "WM/MCDI", QT_TRANSLATE_NOOP("@default", "MCDI") }, @@ -1080,7 +1100,7 @@ QStringList Frame::getNamesForCustomFrames() */ QString Frame::Field::getFieldIdName(FieldId type) { - Q_STATIC_ASSERT(std::size(fieldIdNames) == ID_Seller + 2); + Q_STATIC_ASSERT(std::size(fieldIdNames) == ID_Default + 2); if (static_cast<int>(type) >= 0 && static_cast<int>(type) < static_cast<int>(std::size(fieldIdNames) - 1)) { return QCoreApplication::translate("@default", fieldIdNames[type]); @@ -1200,6 +1220,30 @@ const char* const* Frame::Field::getContentTypeNames() return contentTypeNames; } +/** + * Get a translated string for a target type. + * + * @param type target type / 10 + * + * @return target type, null string if unknown. + */ +QString Frame::Field::getTargetTypeName(int type) +{ + if (type >= 0 && + static_cast<unsigned int>(type) < std::size(targetTypeNames) - 1) { + return QCoreApplication::translate("@default", targetTypeNames[type]); + } + return QString(); +} + +/** + * List of target type strings, NULL terminated. + */ +const char* const* Frame::Field::getTargetTypeNames() +{ + return targetTypeNames; +} + /** * Compare two field lists in a tolerant way. * This function can be used instead of the standard QList equality diff --git a/src/core/tags/frame.h b/src/core/tags/frame.h index 7f020397..03fafd95 100644 --- a/src/core/tags/frame.h +++ b/src/core/tags/frame.h @@ -167,6 +167,10 @@ public: ID_Date, ID_Seller, + // Additional fields for Matroska + ID_TargetType, + ID_Default, + // Additional field for METADATA_BLOCK_PICTURE ID_ImageProperties, @@ -402,6 +406,20 @@ public: */ static const char* const* getContentTypeNames(); + /** + * Get a translated string for a target type. + * + * @param type target type / 10 + * + * @return target type, null string if unknown. + */ + static QString getTargetTypeName(int type); + + /** + * List of target type strings, NULL terminated. + */ + static const char* const* getTargetTypeNames(); + /** * Compare two field lists in a tolerant way. * This function can be used instead of the standard QList equality diff --git a/src/core/tags/pictureframe.cpp b/src/core/tags/pictureframe.cpp index ca7106ff..0803bdb4 100644 --- a/src/core/tags/pictureframe.cpp +++ b/src/core/tags/pictureframe.cpp @@ -365,7 +365,7 @@ void PictureFrame::getFields(const Frame& frame, } break; default: - qDebug("Unknown picture field ID"); + ; } } } diff --git a/src/gui/dialogs/editframefieldsdialog.cpp b/src/gui/dialogs/editframefieldsdialog.cpp index 22867624..2d5979cf 100644 --- a/src/gui/dialogs/editframefieldsdialog.cpp +++ b/src/gui/dialogs/editframefieldsdialog.cpp @@ -37,6 +37,7 @@ #include <QFile> #include <QDir> #include <QBuffer> +#include <QCheckBox> #include <QVBoxLayout> #include <QMimeData> #include <QMimeDatabase> @@ -647,6 +648,66 @@ QWidget* IntComboBoxControl::createWidget(QWidget* parent) } +/** Control to edit boolean fields */ +class BoolFieldControl : public Mp3FieldControl { +public: + /** + * Constructor. + * @param field field to edit + */ + explicit BoolFieldControl(Frame::Field& field) + : Mp3FieldControl(field), m_boolInp(nullptr) {} + + /** + * Destructor. + */ + ~BoolFieldControl() override = default; + + /** + * Update field from data in field control. + */ + void updateTag() override; + + /** + * Create widget to edit field data. + * + * @param parent parent widget + * + * @return widget to edit field data. + */ + QWidget* createWidget(QWidget* parent) override; + +protected: + QCheckBox* m_boolInp; + +private: + Q_DISABLE_COPY(BoolFieldControl) +}; + +/** + * Update field with data from dialog. + */ +void BoolFieldControl::updateTag() +{ + m_field.m_value = m_boolInp->isChecked(); +} + +/** + * Create widget for dialog. + * + * @param parent parent widget + * @return widget to edit field. + */ +QWidget* BoolFieldControl::createWidget(QWidget* parent) +{ + m_boolInp = new QCheckBox(parent); + m_boolInp->setText(Frame::Field::getFieldIdName( + static_cast<Frame::FieldId>(m_field.m_id))); + m_boolInp->setChecked(m_field.m_value.toBool()); + return m_boolInp; +} + + /** Control to import, export and view data from binary fields */ class BinFieldControl : public Mp3FieldControl { public: @@ -1402,12 +1463,28 @@ void EditFrameFieldsDialog::setFrame(const Frame& frame, fld, Frame::Field::getContentTypeNames()); m_fieldcontrols.append(cbox); } + else if (fld.m_id == Frame::ID_TargetType) { + auto cbox = new IntComboBoxControl( + fld, Frame::Field::getTargetTypeNames()); + m_fieldcontrols.append(cbox); + } else { auto intctl = new IntFieldControl(fld); m_fieldcontrols.append(intctl); } break; +#if QT_VERSION >= 0x060000 + case QMetaType::Bool: +#else + case QVariant::Bool: +#endif + { + auto boolctl = new BoolFieldControl(fld); + m_fieldcontrols.append(boolctl); + break; + } + #if QT_VERSION >= 0x060000 case QMetaType::QString: #else diff --git a/src/plugins/taglibmetadata/CMakeLists.txt b/src/plugins/taglibmetadata/CMakeLists.txt index 1c1b6545..a12450ee 100644 --- a/src/plugins/taglibmetadata/CMakeLists.txt +++ b/src/plugins/taglibmetadata/CMakeLists.txt @@ -38,6 +38,11 @@ if(WITH_TAGLIB OR TAGLIB_LIBRARIES) taglibdsfsupport.cpp taglibgenericsupport.cpp ) + if(NOT ${TAGLIB_VERSION} VERSION_LESS 2.2.0) + target_sources(${plugin_TARGET} PRIVATE + taglibmatroskasupport.cpp + ) + endif() if(TAGLIB_VERSION VERSION_LESS 2.0.0) target_sources(${plugin_TARGET} PRIVATE taglibext/aac/aacfiletyperesolver.cpp diff --git a/src/plugins/taglibmetadata/taglibfile.cpp b/src/plugins/taglibmetadata/taglibfile.cpp index d2c719cb..fd3bd612 100644 --- a/src/plugins/taglibmetadata/taglibfile.cpp +++ b/src/plugins/taglibmetadata/taglibfile.cpp @@ -49,6 +49,9 @@ #include "taglibasfsupport.h" #include "taglibmodsupport.h" #include "taglibdsfsupport.h" +#if TAGLIB_VERSION >= 0x020200 +#include "taglibmatroskasupport.h" +#endif #include "taglibgenericsupport.h" using namespace TagLibUtils; @@ -993,6 +996,9 @@ void TagLibFile::staticInit() new TagLibAsfSupport, new TagLibModSupport, new TagLibDsfSupport, +#if TAGLIB_VERSION >= 0x020200 + new TagLibMatroskaSupport, +#endif // It is essential that TagLibGenericSupport is last to provide defaults for // writeFile() and getFrameIds(). new TagLibGenericSupport diff --git a/src/plugins/taglibmetadata/taglibfileiostream.cpp b/src/plugins/taglibmetadata/taglibfileiostream.cpp index 06c3828f..acfacce3 100644 --- a/src/plugins/taglibmetadata/taglibfileiostream.cpp +++ b/src/plugins/taglibmetadata/taglibfileiostream.cpp @@ -262,6 +262,12 @@ TagLib::File* FileIOStream::createFromContents(TagLib::IOStream* stream) { "audio/x-wav", "WAV" }, { "audio/x-wavpack", "WV" }, { "audio/x-xm", "XM" }, +#if TAGLIB_VERSION >= 0x020200 + { "audio/x-matroska", "MKA" }, + { "video/x-matroska", "MKV" }, + { "audio/webm", "WEBM" }, + { "video/webm", "WEBM" }, +#endif { "video/mp4", "MP4" } }; diff --git a/src/plugins/taglibmetadata/taglibmatroskasupport.cpp b/src/plugins/taglibmetadata/taglibmatroskasupport.cpp new file mode 100644 index 00000000..b4187f02 --- /dev/null +++ b/src/plugins/taglibmetadata/taglibmatroskasupport.cpp @@ -0,0 +1,802 @@ +/** + * \file taglibmatroskasupport.cpp + * Support for Matroska files and tags. + * + * \b Project: Kid3 + * \author Urs Fleisch + * \date 24 Dec 2025 + * + * Copyright (C) 2025 Urs Fleisch + * + * This file is part of Kid3. + * + * Kid3 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Kid3 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "taglibmatroskasupport.h" + +#include <QJsonDocument> +#include <matroskafile.h> +#include <matroskaattachments.h> +#include <matroskaattachedfile.h> +#include <matroskachapter.h> +#include <matroskachapters.h> +#include <matroskachapteredition.h> +#include <matroskasimpletag.h> +#include <matroskatag.h> + +#include "pictureframe.h" +#include "taglibutils.h" +#include "taglibfile.h" + +using namespace TagLibUtils; + +namespace { + +constexpr struct { + const char* name; + TagLib::Matroska::SimpleTag::TargetTypeValue targetType; + bool strict; +} matroskaNamesForTypes[] = { + {"TITLE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Title, + {"ARTIST", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Artist, + {"TITLE", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, true}, // FT_Album, + {"COMMENT", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Comment, + {"DATE_RECORDED", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Date, + {"PART_NUMBER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Track, + {"GENRE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Genre, + // FT_LastV1Frame = FT_Track, + {"ARTIST", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, true}, // FT_AlbumArtist, + {"ARRANGER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Arranger, + {"WRITTEN_BY", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Author, + {"BPM", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Bpm, + {"CATALOG_NUMBER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_CatalogNumber, + {"COMPILATION", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Compilation, + {"COMPOSER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Composer, + {"CONDUCTOR", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Conductor, + {"COPYRIGHT", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Copyright, + {"PART_NUMBER", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, true}, // FT_Disc, + {"ENCODER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_EncodedBy, + {"ENCODER_SETTINGS", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_EncoderSettings, + {"DATE_ENCODED", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_EncodingTime, + {"GROUPING", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Grouping, + {"INITIAL_KEY", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_InitialKey, + {"ISRC", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Isrc, + {"LANGUAGE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Language, + {"LYRICIST", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Lyricist, + {"LYRICS", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Lyrics, + {"ORIGINAL_MEDIA_TYPE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Media, + {"MOOD", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Mood, + {"ORIGINALALBUM", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_OriginalAlbum, + {"ORIGINALARTIST", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_OriginalArtist, + {"ORIGINALDATE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_OriginalDate, + {"DESCRIPTION", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Description, + {"PERFORMER", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Performer, + {"PICTURE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Picture, + {"LABEL_CODE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Publisher, + {"RELEASECOUNTRY", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_ReleaseCountry, + {"REMIXED_BY", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Remixer, + {"TITLESORT", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, true}, // FT_SortAlbum, + {"ARTISTSORT", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, true}, // FT_SortAlbumArtist, + {"ARTISTSORT", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_SortArtist, + {"COMPOSERSORT", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_SortComposer, + {"TITLESORT", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_SortName, + {"SUBTITLE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Subtitle, + {"WEBSITE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Website, + {"WWWAUDIOFILE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_WWWAudioFile, + {"WWWAUDIOSOURCE", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_WWWAudioSource, + {"DATE_RELEASED", TagLib::Matroska::SimpleTag::TargetTypeValue::Album, false}, // FT_ReleaseDate, + {"RATING", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Rating, + {"WORK", TagLib::Matroska::SimpleTag::TargetTypeValue::Track, false}, // FT_Work, + // FT_Custom1 +}; + +/** + * Get name of frame from type. + * + * @param type type + * @param targetType the target type is returned here + * + * @return name. + */ +const char* getMatroskaNameFromType(Frame::Type type, + TagLib::Matroska::SimpleTag::TargetTypeValue& targetType) +{ + Q_STATIC_ASSERT(std::size(matroskaNamesForTypes) == Frame::FT_Custom1); + if (Frame::isCustomFrameType(type)) { + targetType = TagLib::Matroska::SimpleTag::TargetTypeValue::Track; + return Frame::getNameForCustomFrame(type); + } + if (type < Frame::FT_Custom1) { + auto [name, targetTypeValue, strict] = matroskaNamesForTypes[type]; + targetType = targetTypeValue; + return name; + } + targetType = TagLib::Matroska::SimpleTag::TargetTypeValue::None; + return "UNKNOWN"; +} + +/** + * Get the frame type for a Matroska name. + * + * @param name Matroska simple tag name + * @param targetType Matroska target type value + * + * @return frame type. + */ +Frame::Type getTypeFromMatroskaName(const QString& name, TagLib::Matroska::SimpleTag::TargetTypeValue targetType) +{ + int i = 0; + for (const auto& nameTarget : matroskaNamesForTypes) { + if (name == QString::fromUtf8(nameTarget.name) && + (targetType == nameTarget.targetType || + (targetType == TagLib::Matroska::SimpleTag::TargetTypeValue::None && + !nameTarget.strict))) { + return static_cast<Frame::Type>(i); + } + ++i; + } + return Frame::getTypeFromCustomFrameName(name.toLatin1()); +} + +QString getMatroskaName(const Frame& frame, + TagLib::Matroska::SimpleTag::TargetTypeValue& targetType) +{ + if (Frame::Type type = frame.getType(); type <= Frame::FT_LastFrame) { + return QString::fromLatin1(getMatroskaNameFromType(type, targetType)); + } + targetType = TagLib::Matroska::SimpleTag::TargetTypeValue::Track; + return TaggedFile::fixUpTagKey(frame.getName(), TaggedFile::TT_Vorbis).toUpper(); +} + +QString toSimpleTextOrJson(const QVariantMap& metadata) +{ + if (metadata.isEmpty()) { + return {}; + } + if (metadata.size() == 1) { + if (const QVariant& firstValue = metadata.first(); +#if QT_VERSION >= 0x060000 + firstValue.typeId() == QMetaType::QString +#else + firstValue.type() == QVariant::String +#endif + ) { + return firstValue.toString(); + } + } + return QString::fromUtf8(QJsonDocument::fromVariant(metadata) + .toJson(QJsonDocument::Compact)); +} + +QVariantMap fromSimpleTextOrJson(const QString& str) +{ + if (str.startsWith(QLatin1Char('{')) && str.endsWith(QLatin1Char('}'))) { + return QJsonDocument::fromJson(str.toUtf8()).toVariant().toMap(); + } + return {{QLatin1String("text"), str}}; +} + +void matroskaPictureToFrame( + const TagLib::Matroska::AttachedFile& attachedFile, Frame& frame) +{ + const TagLib::ByteVector& bv = attachedFile.data(); + const QByteArray data(bv.data(), static_cast<int>(bv.size())); + const QString& mediaType = toQString(attachedFile.mediaType()); + const QString& description = toQString(attachedFile.description()); + const QString& fileName = toQString(attachedFile.fileName()); + const QString& uid = QString::number(attachedFile.uid()); + PictureFrame::setFields( + frame, Frame::TE_ISO8859_1, QLatin1String("JPG"), mediaType, + PictureFrame::PT_CoverFront, description, data); + frame.fieldList().append({Frame::ID_Filename, fileName}); + frame.fieldList().append({Frame::ID_Id, uid}); +} + +TagLib::Matroska::AttachedFile frameToMatroskaPicture(const Frame& frame) +{ + Frame::TextEncoding enc; + PictureFrame::PictureType pictureType; + QByteArray data; + QString imgFormat, mimeType, description; + PictureFrame::getFields(frame, enc, imgFormat, mimeType, pictureType, + description, data); + const QString fileName = Frame::getField(frame, Frame::ID_Filename).toString(); + const qulonglong uid = Frame::getField(frame, Frame::ID_Id).toULongLong(); + return TagLib::Matroska::AttachedFile( + TagLib::ByteVector(data.constData(), static_cast<unsigned int>(data.size())), + toTString(fileName), toTString(mimeType), uid, toTString(description)); +} + +void matroskaAttachedFileToFrame( + const TagLib::Matroska::AttachedFile& attachedFile, Frame& frame) +{ + const TagLib::ByteVector& bv = attachedFile.data(); + const QByteArray data(bv.data(), static_cast<int>(bv.size())); + const QString& mediaType = toQString(attachedFile.mediaType()); + const QString fileName = toQString(attachedFile.fileName()); + const QString& description = toQString(attachedFile.description()); + const QString& uid = QString::number(attachedFile.uid()); + frame.setExtendedType( + Frame::ExtendedType(Frame::FT_Other, QLatin1String("General Object"))); + frame.setValue(description); + // The fields for non-picture attachments are the same as for the + // ID3 GEOB frame plus the UID as an ID. + frame.fieldList() = { + {Frame::ID_TextEnc, Frame::TE_ISO8859_1}, + {Frame::ID_MimeType, mediaType}, + {Frame::ID_Filename, fileName}, + {Frame::ID_Description, description}, + {Frame::ID_Data, data}, + {Frame::ID_Id, uid} + }; +} + +TagLib::Matroska::AttachedFile frameToMatroskaAttachedFile(const Frame& frame) +{ + QByteArray data; + QString mimeType, description; + PictureFrame::getData(frame, data); + PictureFrame::getMimeType(frame, mimeType); + PictureFrame::getDescription(frame, description); + const QString fileName = Frame::getField(frame, Frame::ID_Filename).toString(); + const qulonglong uid = Frame::getField(frame, Frame::ID_Id).toULongLong(); + return TagLib::Matroska::AttachedFile( + TagLib::ByteVector(data.constData(), static_cast<unsigned int>(data.size())), + toTString(fileName), toTString(mimeType), uid, toTString(description)); +} + +void matroskaChapterEditionToFrame( + const TagLib::Matroska::ChapterEdition& chapterEdition, Frame& frame) +{ + const QString& uid = QString::number(chapterEdition.uid()); + QVariantMap editionMap; + if (!chapterEdition.isDefault()) { + editionMap.insert(QLatin1String("default"), chapterEdition.isDefault()); + } + if (chapterEdition.isOrdered()) { + editionMap.insert(QLatin1String("ordered"), chapterEdition.isOrdered()); + } + const QString description = toSimpleTextOrJson(editionMap); + frame.setExtendedType( + Frame::ExtendedType(Frame::FT_Other, QLatin1String("Chapters"))); + frame.setValue(description); + + TagLib::String language; + QVariantList synchedData; + + unsigned long long lastTimeEnd = 0ULL; + + unsigned long long chapterNr = 1ULL; + for (const auto& chapter : chapterEdition.chapterList()) { + if (lastTimeEnd && lastTimeEnd != chapter.timeStart()) { + synchedData.append(static_cast<double>(lastTimeEnd) / 1E6); + synchedData.append(QString()); + } + synchedData.append(static_cast<double>(chapter.timeStart()) / 1E6); + QVariantMap chapMap; + for (const auto& display : chapter.displayList()) { + if (language.isEmpty()) { + language = display.language(); + } + chapMap.insert(toQString(display.language()), toQString(display.string())); + } + if (chapter.uid() != chapterNr) { + chapMap.insert(QLatin1String("uid"), chapter.uid()); + } + if (chapter.isHidden()) { + chapMap.insert(QLatin1String("hidden"), chapter.isHidden()); + } + synchedData.append(toSimpleTextOrJson(chapMap)); + lastTimeEnd = chapter.timeEnd(); + ++chapterNr; + } + // synchedData.append(static_cast<quint32>(lastTimeEnd / 1000000ULL)); + synchedData.append(static_cast<double>(lastTimeEnd) / 1E6); + synchedData.append(QString()); + + // The fields for chapters are the same as for the ID3 SYLT frame. + frame.fieldList() = { + {Frame::ID_TextEnc, Frame::TE_UTF8}, + {Frame::ID_Language, toQString(language)}, + {Frame::ID_TimestampFormat, 2}, // milliseconds as unit + {Frame::ID_ContentType, 0}, // other + {Frame::ID_Description, description}, + {Frame::ID_Id, uid}, + {Frame::ID_Data, synchedData} + }; +} + +TagLib::Matroska::ChapterEdition frameToMatroskaChapterEdition(const Frame& frame) +{ + const TagLib::String language = toTString(Frame::getField(frame, Frame::ID_Language).toString()); + const QVariantList synchedData = Frame::getField(frame, Frame::ID_Data).toList(); + + struct ChapterData { + TagLib::List<TagLib::Matroska::Chapter::Display> displays; + unsigned long long timeStart; + unsigned long long timeEnd; + unsigned long long uid; + bool hidden; + }; + QList<ChapterData> chapterData; + unsigned long long chapterNr = 1ULL; + int chapterDataIndex = -1; + + QListIterator it(synchedData); + while (it.hasNext()) { + auto time = static_cast<unsigned long long>(it.next().toDouble() * 1E6); + auto text = it.next().toString(); + if (chapterDataIndex >= 0 && !chapterData[chapterDataIndex].timeEnd) { + chapterData[chapterDataIndex].timeEnd = time; + if (text.isEmpty()) { + continue; + } + } + auto map = fromSimpleTextOrJson(text); + ChapterData cd; + cd.timeStart = time; + cd.timeEnd = 0; + cd.uid = map.take(QLatin1String("uid")).toULongLong(); + if (!cd.uid) { + cd.uid = chapterNr; + } + cd.hidden = map.take(QLatin1String("hidden")).toBool(); + if (!map.isEmpty()) { + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + cd.displays.append(TagLib::Matroska::Chapter::Display( + toTString(it.value().toString()), + it.key() != QLatin1String("text") ? toTString(it.key()) : language)); + } + } else { + cd.displays.append(TagLib::Matroska::Chapter::Display("", language)); + } + chapterData.append(cd); + ++chapterNr; + ++chapterDataIndex; + } + + TagLib::List<TagLib::Matroska::Chapter> chapters; + for (const auto& cd : chapterData) { + chapters.append(TagLib::Matroska::Chapter( + cd.timeStart, cd.timeEnd, cd.displays, cd.uid, cd.hidden)); + } + const qulonglong uid = Frame::getField(frame, Frame::ID_Id).toULongLong(); + const QString description = Frame::getField(frame, Frame::ID_Description).toString(); + auto map = fromSimpleTextOrJson(description); + return TagLib::Matroska::ChapterEdition( + chapters, + map.value(QLatin1String("default"), true).toBool(), + map.value(QLatin1String("ordered"), false).toBool(), + uid); +} + +TagLib::Matroska::SimpleTag frameToMatroskaSimpleTag(const Frame& frame) +{ + const QVariant dataVar = Frame::getField(frame, Frame::ID_Data); + const bool isBinary = dataVar.isValid(); + const QByteArray data = isBinary ? dataVar.toByteArray() : QByteArray(); + const TagLib::String name = toTString(frame.getInternalName()); + const TagLib::String value = toTString(frame.getValue()); + auto targetType = + static_cast<TagLib::Matroska::SimpleTag::TargetTypeValue>( + Frame::getField(frame, Frame::ID_TargetType).toInt() * 10); + const TagLib::String language = toTString( + Frame::getField(frame, Frame::ID_Language).toString()); + const bool defaultLanguage = + Frame::getField(frame, Frame::ID_Default).toBool(); + const unsigned long long trackUid = + Frame::getField(frame, Frame::ID_Id).toULongLong(); + return !isBinary + ? TagLib::Matroska::SimpleTag( + name, value, targetType, language, defaultLanguage, trackUid) + : TagLib::Matroska::SimpleTag( + name, TagLib::ByteVector(data.constData(), data.size()), + targetType, language, defaultLanguage, trackUid); +} + +bool isExtraFrame(Frame::Type type, const QString& name) +{ + return type == Frame::FT_Picture || + (type == Frame::FT_Other && ( + name == QLatin1String("General Object") || + name == QLatin1String("Chapters"))); +} + +bool isExtraFrame(const Frame::ExtendedType& type) +{ + return isExtraFrame(type.getType(), type.getInternalName()); +} + +} + + +TagLib::File* TagLibMatroskaSupport::createFromExtension( + TagLib::IOStream* stream, const TagLib::String& ext) const +{ + if (ext == "MKA" || ext == "MKV" || ext == "WEBM") + return new TagLib::Matroska::File(stream); + return nullptr; +} + +bool TagLibMatroskaSupport::readFile(TagLibFile& f, TagLib::File* file) const +{ + if (auto mkaFile = dynamic_cast<TagLib::Matroska::File*>(file)) { + f.m_fileExtension = QLatin1String(".mka"); + putFileRefTagInTag2(f); + + if (!f.m_extraFrames.isRead()) { + int i = 0; + if (auto attachments = mkaFile->attachments()) { + for (const auto& attachedFile : attachments->attachedFileList()) { + if (attachedFile.mediaType().startsWith("image/")) { + PictureFrame frame; + matroskaPictureToFrame(attachedFile, frame); + frame.setIndex(Frame::toNegativeIndex(i++)); + f.m_extraFrames.append(frame); + } else { + Frame frame; + matroskaAttachedFileToFrame(attachedFile, frame); + frame.setIndex(Frame::toNegativeIndex(i++)); + f.m_extraFrames.append(frame); + } + } + } + if (auto chapters = mkaFile->chapters()) { + for (const auto& chapterEdition : chapters->chapterEditionList()) { + Frame frame; + matroskaChapterEditionToFrame(chapterEdition, frame); + frame.setIndex(Frame::toNegativeIndex(i++)); + f.m_extraFrames.append(frame); + } + } + f.m_extraFrames.setRead(true); + } + return true; + } + return false; +} + + +bool TagLibMatroskaSupport::writeFile(TagLibFile& f, TagLib::File* file, bool force, + int, bool& fileChanged) const +{ + if (auto mkaFile = dynamic_cast<TagLib::Matroska::File*>(file)) { + if (anyTagMustBeSaved(f, force)) { + if (auto attachments = mkaFile->attachments(false)) { + attachments->clear(); + } + if (auto chapters = mkaFile->chapters(false)) { + chapters->clear(); + } + const auto frames = f.m_extraFrames; + for (const Frame& frame : frames) { + if (frame.getExtendedType() == Frame::ExtendedType( + Frame::FT_Other, QLatin1String("Chapters"))) { + mkaFile->chapters(true)->addChapterEdition( + frameToMatroskaChapterEdition(frame)); + } else if (frame.getType() == Frame::FT_Picture) { + mkaFile->attachments(true)->addAttachedFile( + frameToMatroskaPicture(frame)); + } else { + mkaFile->attachments(true)->addAttachedFile( + frameToMatroskaAttachedFile(frame)); + } + } + if (saveFileRef(f)) { + fileChanged = true; + } + } + return true; + } + return false; +} + +bool TagLibMatroskaSupport::makeTagSettable(TagLibFile& f, TagLib::File* file, + Frame::TagNumber tagNr) const +{ + if (TagLib::Matroska::File* mkaFile; + tagNr == Frame::Tag_2 && + (mkaFile = dynamic_cast<TagLib::Matroska::File*>(file)) != nullptr) { + f.m_tag[tagNr] = mkaFile->tag(true); + return true; + } + return false; +} + +bool TagLibMatroskaSupport::readAudioProperties( + TagLibFile& f, TagLib::AudioProperties* audioProperties) const +{ + if (auto mkaProperties = dynamic_cast<TagLib::Matroska::Properties*>(audioProperties)) { + f.m_detailInfo.format = toQString( + mkaProperties->docType().substr(0, 1).upper() + + mkaProperties->docType().substr(1)); + f.m_detailInfo.format += QLatin1String(" Version ") + + QString::number(mkaProperties->docTypeVersion()); + if (!mkaProperties->codecName().isEmpty()) { + f.m_detailInfo.format += QLatin1String(" Codec ") + + toQString(mkaProperties->codecName()); + } + return true; + } + return false; +} + +QString TagLibMatroskaSupport::getTagFormat( + const TagLib::Tag* tag, TaggedFile::TagType&) const +{ + if (dynamic_cast<const TagLib::Matroska::Tag*>(tag) != nullptr) { + return QLatin1String("Matroska"); + } + return {}; +} + +bool TagLibMatroskaSupport::setFrame(TagLibFile& f, Frame::TagNumber tagNr, + const Frame& frame) const +{ + if (auto mkaTag = dynamic_cast<TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + if (int index = frame.getIndex(); index != -1) { + if (Frame::ExtendedType extendedType = frame.getExtendedType(); + isExtraFrame(extendedType)) { + if (f.m_extraFrames.isRead()) { + if (int idx = Frame::fromNegativeIndex(frame.getIndex()); + idx >= 0 && idx < f.m_extraFrames.size()) { + if (Frame newFrame(frame); + PictureFrame::areFieldsEqual(f.m_extraFrames[idx], newFrame)) { + f.m_extraFrames[idx].setValueChanged(false); + } else { + f.m_extraFrames[idx] = newFrame; + f.markTagChanged(tagNr, extendedType); + } + return true; + } + return false; + } + } + if (index < static_cast<int>(mkaTag->simpleTagsList().size())) { + mkaTag->removeSimpleTag(index); + mkaTag->insertSimpleTag(index, frameToMatroskaSimpleTag(frame)); + f.markTagChanged(tagNr, frame.getExtendedType()); + } + return true; + } + return setFrameWithoutIndex(f, tagNr, frame); + } + return false; +} + +bool TagLibMatroskaSupport::addFrame(TagLibFile& f, Frame::TagNumber tagNr, Frame& frame) const +{ + if (auto mkaTag = dynamic_cast<TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + if (Frame::ExtendedType extendedType = frame.getExtendedType(); + isExtraFrame(extendedType)) { + if (frame.getFieldList().isEmpty()) { + if (extendedType.getType() == Frame::FT_Picture) { + PictureFrame::setFields(frame); + frame.fieldList().append({ + {Frame::ID_Filename, QString()}, + {Frame::ID_Id, QString()} + }); + } else if (extendedType.getName() == QLatin1String("General Object")) { + frame.fieldList() = { + {Frame::ID_TextEnc, Frame::TE_ISO8859_1}, + {Frame::ID_MimeType, QString()}, + {Frame::ID_Filename, QString()}, + {Frame::ID_Description, QString()}, + {Frame::ID_Data, QByteArray()}, + {Frame::ID_Id, QString()} + }; + } else { + frame.fieldList() = { + {Frame::ID_TextEnc, Frame::TE_UTF8}, + {Frame::ID_Language, QString()}, + {Frame::ID_TimestampFormat, 2}, // milliseconds as unit + {Frame::ID_ContentType, 0}, // other + {Frame::ID_Description, QString()}, + {Frame::ID_Id, QString()}, + {Frame::ID_Data, QVariantList()} + }; + } + } + if (f.m_extraFrames.isRead()) { + frame.setIndex(Frame::toNegativeIndex(static_cast<int>(f.m_extraFrames.size()))); + f.m_extraFrames.append(frame); + f.markTagChanged(tagNr, extendedType); + return true; + } + } + + // Add a Matroska simple tag for the given frame. + // To create simple tags with binary contents, " - binary" can be appended + // to the name, it will be stripped away. + bool isBinary = false; + if (QString internalName = frame.getInternalName(); + internalName.endsWith(QLatin1String(" - binary"))) { + isBinary = true; + internalName.truncate(internalName.length() - 9); + frame.setExtendedType(Frame::ExtendedType(frame.getType(), internalName)); + } + TagLib::Matroska::SimpleTag::TargetTypeValue targetType; + QString name = getMatroskaName(frame, targetType); + frame.setExtendedType(Frame::ExtendedType(frame.getType(), name)); + if (!isBinary) { + frame.fieldList() = {{Frame::ID_Text, frame.getValue()}}; + } else { + frame.fieldList() = {{Frame::ID_Data, QByteArray()}}; + } + frame.fieldList().append({ + {Frame::ID_TargetType, static_cast<int>(targetType) / 10}, + {Frame::ID_Language, QLatin1String("en")}, + {Frame::ID_Default, true}, + {Frame::ID_Id, QLatin1String("0")} + }); + frame.setIndex(mkaTag->simpleTagsList().size()); + mkaTag->addSimpleTag(frameToMatroskaSimpleTag(frame)); + f.markTagChanged(tagNr, frame.getExtendedType()); + return true; + } + return false; +} + +bool TagLibMatroskaSupport::deleteFrame(TagLibFile& f, Frame::TagNumber tagNr, + const Frame& frame) const +{ + if (auto mkaTag = dynamic_cast<TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + if (Frame::ExtendedType extendedType = frame.getExtendedType(); + isExtraFrame(extendedType)) { + if (f.m_extraFrames.isRead()) { + if (int idx = Frame::fromNegativeIndex(frame.getIndex()); + idx >= 0 && idx < f.m_extraFrames.size()) { + f.m_extraFrames.removeAt(idx); + while (idx < f.m_extraFrames.size()) { + f.m_extraFrames[idx].setIndex(Frame::toNegativeIndex(idx)); + ++idx; + } + f.markTagChanged(tagNr, extendedType); + return true; + } + } + } + if (int index = frame.getIndex(); + index >= 0 && index < static_cast<int>(mkaTag->simpleTagsList().size())) { + mkaTag->removeSimpleTag(index); + f.markTagChanged(tagNr, frame.getExtendedType()); + } + } + return false; +} + +bool TagLibMatroskaSupport::deleteFrames( + TagLibFile& f, Frame::TagNumber tagNr, const FrameFilter& flt) const +{ + if (auto mkaTag = dynamic_cast<TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + if (flt.areAllEnabled()) { + mkaTag->clearSimpleTags(); + f.m_extraFrames.clear(); + f.markTagChanged(tagNr, Frame::ExtendedType()); + } else { + TagLib::Matroska::SimpleTagsList simpleTags = mkaTag->simpleTagsList(); + bool simpleTagRemoved = false; + for (auto it = simpleTags.begin(); + it != simpleTags.end();) { + QString name = toQString(it->name()); + Frame::Type type = getTypeFromMatroskaName(name, it->targetTypeValue()); + if (flt.isEnabled(type, name)) { + simpleTagRemoved = true; + it = simpleTags.erase(it); + } else { + ++it; + } + } + if (simpleTagRemoved) { + mkaTag->clearSimpleTags(); + mkaTag->addSimpleTags(simpleTags); + } + + bool extraFrameRemoved = false; + if (f.m_extraFrames.isRead()) { + for (auto it = f.m_extraFrames.begin(); + it != f.m_extraFrames.end();) { + if (flt.isEnabled(it->getType(), it->getInternalName())) { + extraFrameRemoved = true; + it = f.m_extraFrames.erase(it); + } else { + ++it; + } + } + if (extraFrameRemoved) { + int i = 0; + for (Frame& frame : f.m_extraFrames) { + frame.setIndex(Frame::toNegativeIndex(i++)); + } + } + } + + if (simpleTagRemoved || extraFrameRemoved) { + f.markTagChanged(tagNr, Frame::ExtendedType()); + } + } + return true; + } + return false; +} + +bool TagLibMatroskaSupport::getAllFrames( + TagLibFile& f, Frame::TagNumber tagNr, FrameCollection& frames) const +{ + if (auto mkaTag = dynamic_cast<const TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + const auto& simpleTags = mkaTag->simpleTagsList(); + int i = 0; + for (const auto& simpleTag : simpleTags) { + const QString name = toQString(simpleTag.name()); + Frame::Type type = getTypeFromMatroskaName(name, simpleTag.targetTypeValue()); + QString value; + if (simpleTag.type() == TagLib::Matroska::SimpleTag::StringType) { + value = toQString(simpleTag.toString()); + } + Frame frame(type, value, name, i++); + if (simpleTag.type() == TagLib::Matroska::SimpleTag::StringType) { + frame.fieldList().append({Frame::ID_Text, value}); + } else if (simpleTag.type() == TagLib::Matroska::SimpleTag::BinaryType) { + const TagLib::ByteVector bv = simpleTag.toByteVector(); + frame.fieldList().append( + {Frame::ID_Data, QByteArray(bv.data(), bv.size())}); + } + frame.fieldList().append({ + {Frame::ID_TargetType, static_cast<int>(simpleTag.targetTypeValue()) / 10}, + {Frame::ID_Language, toQString(simpleTag.language())}, + {Frame::ID_Default, simpleTag.defaultLanguageFlag()}, + {Frame::ID_Id, QString::number(simpleTag.trackUid())} + }); + frames.insert(frame); + } + if (f.m_extraFrames.isRead()) { + for (auto it = f.m_extraFrames.constBegin(); + it != f.m_extraFrames.constEnd(); + ++it) { + frames.insert(*it); + } + } + return true; + } + return false; +} + +QStringList TagLibMatroskaSupport::getFrameIds( + const TagLibFile& f, Frame::TagNumber tagNr) const +{ + QStringList lst; + if (dynamic_cast<TagLib::Matroska::Tag*>(f.m_tag[tagNr])) { + static const char* const fieldNames[] = { + "DIRECTOR", + "DURATION", + "SUMMARY", + "SYNOPSIS", + "TOTAL_PARTS", + "Chapters", + "General Object" + }; + for (int k = Frame::FT_FirstFrame; k <= Frame::FT_LastFrame; ++k) { + if (auto name = Frame::ExtendedType(static_cast<Frame::Type>(k), + QLatin1String("")).getName(); + !name.isEmpty()) { + lst.append(name); + } + } + for (auto fieldName : fieldNames) { + lst.append(QString::fromLatin1(fieldName)); // clazy:exclude=reserve-candidates + } + } + return lst; +} diff --git a/src/plugins/taglibmetadata/taglibmatroskasupport.h b/src/plugins/taglibmetadata/taglibmatroskasupport.h new file mode 100644 index 00000000..7a810695 --- /dev/null +++ b/src/plugins/taglibmetadata/taglibmatroskasupport.h @@ -0,0 +1,56 @@ +/** + * \file taglibmatroskasupport.h + * Support for Matroska files and tags. + * + * \b Project: Kid3 + * \author Urs Fleisch + * \date 24 Dec 2025 + * + * Copyright (C) 2025 Urs Fleisch + * + * This file is part of Kid3. + * + * Kid3 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Kid3 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "taglibformatsupport.h" + +class TagLibMatroskaSupport : public TagLibFormatSupport { +public: + TagLib::File* createFromExtension(TagLib::IOStream* stream, + const TagLib::String& ext) const override; + bool readFile(TagLibFile& f, TagLib::File* file) const override; + bool writeFile(TagLibFile& f, TagLib::File* file, bool force, + int id3v2Version, bool& fileChanged) const override; + bool makeTagSettable(TagLibFile& f, TagLib::File* file, + Frame::TagNumber tagNr) const override; + bool readAudioProperties(TagLibFile& f, + TagLib::AudioProperties* audioProperties) const override; + QString getTagFormat(const TagLib::Tag* tag, + TaggedFile::TagType& type) const override; + bool setFrame(TagLibFile& f, Frame::TagNumber tagNr, + const Frame& frame) const override; + bool addFrame(TagLibFile& f, Frame::TagNumber tagNr, + Frame& frame) const override; + bool deleteFrame(TagLibFile& f, Frame::TagNumber tagNr, + const Frame& frame) const override; + bool deleteFrames(TagLibFile& f, Frame::TagNumber tagNr, + const FrameFilter& flt) const override; + bool getAllFrames(TagLibFile& f, Frame::TagNumber tagNr, + FrameCollection& frames) const override; + QStringList getFrameIds(const TagLibFile& f, + Frame::TagNumber tagNr) const override; +};
