[multimedia/kid3] /: Matroska support (#435)

Urs Fleisch null at kde.org
Wed Jan 7 05:16:09 GMT 2026


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;
+};



More information about the kde-doc-english mailing list