[multimedia/kid3] src: More tests

Urs Fleisch <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3a61b68f0ceb3e278d9b955c2c9165eef1ae93cf by Urs Fleisch.
Committed on 20/07/2026 at 19:33.
Pushed by ufleisch into branch 'master'.

More tests

M  +2    -2    src/plugins/id3libmetadata/mp3file.cpp
M  +148  -0    src/test/kid3testsupport.py
M  +988  -69   src/test/test_cli_functions.py

https://invent.kde.org/multimedia/kid3/-/commit/3a61b68f0ceb3e278d9b955c2c9165eef1ae93cf

diff --git a/src/plugins/id3libmetadata/mp3file.cpp b/src/plugins/id3libmetadata/mp3file.cpp
index ff904bc5..db5db8b5 100644
--- a/src/plugins/id3libmetadata/mp3file.cpp
+++ b/src/plugins/id3libmetadata/mp3file.cpp
@@ -876,10 +876,10 @@ QString Mp3File::getFileExtension() const
  */
 QString Mp3File::getTagFormat(Frame::TagNumber tagNr) const
 {
-  if (tagNr == Frame::Tag_1 && m_tagV1 && m_tagV1->HasV1Tag()) {
+  if (tagNr == Frame::Tag_1 && m_tagV1 && (m_tagV1->HasV1Tag() || m_tagV2->NumFrames())) {
     return QLatin1String("ID3v1.1");
   }
-  if (tagNr == Frame::Tag_2 && m_tagV2 && m_tagV2->HasV2Tag()) {
+  if (tagNr == Frame::Tag_2 && m_tagV2 && (m_tagV2->HasV2Tag() || m_tagV2->NumFrames())) {
     switch (m_tagV2->GetSpec()) {
     case ID3V2_3_0:
       return QLatin1String("ID3v2.3.0");
diff --git a/src/test/kid3testsupport.py b/src/test/kid3testsupport.py
index 6793eee9..d9ad850c 100644
--- a/src/test/kid3testsupport.py
+++ b/src/test/kid3testsupport.py
@@ -3,9 +3,56 @@ import sys
 import re
 import locale
 import subprocess
+import tempfile
 import base64
 import zlib
 
+
+class Kid3ConfigFile:
+    """Temporary file context manager with INI file contents."""
+    def __init__(self, contents):
+        self._contents = contents
+        self._file = tempfile.NamedTemporaryFile('w+')
+
+    def __enter__(self):
+        result = self._file.__enter__()
+        result.write(self._contents)
+        result.flush()
+        os.environ['KID3_CONFIG_FILE'] = self._file.name
+        return result
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        os.environ['KID3_CONFIG_FILE'] = ''
+        return self._file.__exit__(exc_type, exc_val, exc_tb)
+
+    def __getattr__(self, name):
+        return getattr(self._file, name)
+
+
+class Kid3ConfigFileUsingOnlyTagLib(Kid3ConfigFile):
+    """Context for configuration using only the TaglibMetadata plugin."""
+    def __init__(self):
+        super().__init__('[Tags]\nDisabledPlugins=Id3libMetadata, OggFlacMetadata, Mp4v2Metadata\n')
+
+
+class Kid3ConfigFileUsingOnlyId3lib(Kid3ConfigFile):
+    """Context for configuration using only the Id3libMetadata plugin."""
+    def __init__(self):
+        super().__init__('[Tags]\nDisabledPlugins=TaglibMetadata, OggFlacMetadata, Mp4v2Metadata\n')
+
+
+class Kid3ConfigFileUsingOnlyOggFlac(Kid3ConfigFile):
+    """Context for configuration using only the OggFlacMetadata plugin."""
+    def __init__(self):
+        super().__init__('[Tags]\nDisabledPlugins=TaglibMetadata, Id3libMetadata, Mp4v2Metadata\n')
+
+
+class Kid3ConfigFileUsingOnlyMp4v2(Kid3ConfigFile):
+    """Context for configuration using only the Mp4v2Metadata plugin."""
+    def __init__(self):
+        super().__init__('[Tags]\nDisabledPlugins=TaglibMetadata, Id3libMetadata, OggFlacMetadata\n')
+
+
 _kid3_cli_path = ''
 
 
@@ -159,6 +206,41 @@ def create_test_file(filename):
             b'\x01\x01d\x01D\xac\x00\x00\x00\x00\x00OggS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x91d\x87S\x01\x00' \
             b'\x00\x002\xa1\x1d5\x01\x1bOpusTags\x0b\x00\x00\x00libopus 1.1\x00\x00\x00\x00OggS\x00\x04f\x01\x00\x00' \
             b'\x00\x00\x00\x00\x91d\x87S\x02\x00\x00\x00\xe6\xe1CE\x01\x03\xf8\xff\xfe'
+    elif ext == '.ogg':
+            d = zlib.decompress(base64.b64decode(
+            b'eJztV31QU1cWv4GAASO+QLCBxprgi+SliZNgbImL2+SVaIimwINYPmRXgRTixxaJdGG2nYkk2DRQJpvGtE1TBzBJIRVHaLGWnZ3pWq'
+            b'UqFvmo6LbbrS1dQOrs2PWPOts/ds97gao7nWl3dvtfz9zcd+6559xz7+/c3HtuYV1dCYpDi/T5cB3Dfzvz7ArWQ6xnnm6sttpoQVz+'
+            b'8ZiGfYjFfE+zCmlLdK8l3fP1thwN77F/3U/L4mMjcUFh++5nnnpEtf7R9SrlBtpgDfwsv6l5utbSuBn6aqBPpYFOlWiftTpmxkqIfS'
+            b'Xk4ztYSIsQbhPItoRtq5xYJlmUMSMkx/bezCC381qFOdsrRyU2jzjURboqpYfGPWZEGzyva+Y73TqO2CnZXOZ1ZG8GzqUBmZWWYeRS'
+            b'pdfUQ6+Os9o5o8zlOv6We0cIsjIMZPMi50rydMBxXAN6+EZDkSNRMy90npFL3a1nladXOdkbpR2tXyrPf+RYTroCjoKNdzKddbrmVT'
+            b'EfuM7gcSTqzgucW3Qwg0TNab5zVNeb6ZRpOGucInVtOvIg1NUjdfdIk7HMwWR0HiFMW8z/4IDwwu+FFwaEyzkryhMQYiEO6BncIUPn'
+            b'lwYvXu6VMGX2hah8dJApOYy5AaGiYq7+AH8ZY1eLEKcr1x0uc4fr3XOuTo7z8PE4ZEeIDW7Uj5fkbfXlXy41Xz21/5N7SqXfksGYA4'
+            b'bYpPWpvkSXMj4jkYkDTMPOcyvP2pdAtguMKT1J/ZrPgjNd5SkB44BmddAw1OJ9zf2uZi5oG7swG1SNAdcTLOcGQG8ueLarPDnGZQ+0'
+            b'jAdkNHdg7MauIG8EuHX9LZ7Ai0PAJfRbOwPbBoAzvNtCvbZtiBlvqVo4d0MYTKO53j++639t/BxwhSM3/hrExu7TY0CG9Wqxwy/u/g'
+            b'vBmZyrF23hVWfm3NB9M69MynQEyYWJ+fqQbxNHcuQAZcyOAR6HtBxxG58yLFXVLvFhQeNF4o2uVqOsr7enI2dU/tYlptp08/hbKb/g'
+            b'qs5ijz/cf5Yq2b9xumzv3zVzAcfbOyr29g9RRiLso5IZ/EQwFzIxo9ZTHBuUeCPYmCzrjcEpokODUOE/hBO81XfL18LLPElxKhTFVM'
+            b'7GqRK9ucly1d9U/s5zldfaqr557lMod5p2l/kt5feV2NLhry2CfR5bCDEisF3SzOM+bGmZEFDUwN/K7LeCQXzbJP7hpLzwtrowbXMx'
+            b'Xz+lNk+kVxU/ur/4EcvUxqqSR6rMO/Y/6d9fearpk5ctn3zz3Gf3lKplzBJTEdrkXUt4swjvTG4nXqCtPy9D2kR6s7i/kHbO5HrxAq'
+            b'/E6q1r8fe+Grjx6lFZlFFBGK1CkzvS7O5rdvc2d867vPWv+nv7Ag9fjNwcPvHVD5XlzAbHETqIb23Et1K4ngKGQdfOpdEVvq/C9Y3y'
+            b'0Ul1bI0TaZsB0CcPHro3AAhxL5zkj04ITamSK5TCXKK4QumvlJh/iExTalMaAJuEPCzkwUoUipFAUjtePbnnza4aLnUzPGCQ1Q29c1'
+            b'HVJhj6VebzvmLsd314jVt3SREaJ9b1T5eSbfzQKzWydCqFguC1ISzAut4g1A7KoSweuR71ivG8lSfVKxfbuCleXhivvhyP/nfSTqq1'
+            b't/OgLLbB0a083q183mJbfjlePRGfV8z+CXydBF/5vPG8e3yBo7yJ/8e6fqaf6QcIg3IdfrvE/MzBLORKQw2GVDzJy/4w3uBPMDkif1'
+            b'KzvBbdVJdVabprhUOHbkrkRgXQMa52Zd3t8kZoZUxvD0FHfeZ3/yGgy2qxG9O/su69DSZXVj4vkvT9U4KTWdAAH2U397s55botLGSf'
+            b'yfXD2d05U7YKoTK/xI3QrvKANO77h/nxdL/DVOSKQw1ScPjBgPw6eMQvDCJ/wniePcKC1YCylFY+qSupPmCHgz8OFSOUeG5AbkrTe0'
+            b'PWQLYbL5jKK+RbA3MtR61nbufRy+6pD2D6YrXplYg1KlOazHA/FTGwOyJnv0Bth4+zERmHhO7q8sBsy9Fsd6QvEBtFP6E2lZgLvJGW'
+            b'o5AsWQOR6KDcdDuPvqJKzea2RXE0ugCOGG3zlabP6RHAY3RhmHE6VWoyN1WJ3BFrIBqNnlieX8iHMauuNh2ae4G2fXN4PWaih2uqKk'
+            b'+kZxj96mKO1gwjVT3pZ9EmXw0PcovMV6Hfb0F0Sz4KqKcI4SLt6HqpQ9Z+csK5fKW2W/xwnF2K0HuCCL/konKdOIRljuBOPrlOEp6k'
+            b'oqIQRRHpzlKjTHLMR+0hzBhF9PmSSZnkCIXQtUPx8Gsrqj9vRKKVyK7cQl/aT2SF+DVjmmwe5VbNBV8yqmYhvSkgMuA2nwv5mGyHMt'
+            b'IyDRFm8h4fZQTZE0ZC4hzTZYd9kyBLQ6iUQfvY+Vn2Wh0DN8VC3M4aGm5Ayo8z0ZIGIusKYgGIXljC88jcIsayq+mQF5jM5qpBOYPl'
+            b'wrCShg1giYE2vH5fJY2aJeNUP0QEMNt5tcnyZ/8h+R9O3Px4EKFb7GWoK+7BHVtziczlcnWthL4vtQKHB7JxC0Ymi50+ipP0H7vRAF'
+            b'u+Y2CaTqimtXVQAWe/xDTvVtT9zR9VKSA5YzK0e6oE9E9zLrrz2LWTm84UTJTtgoiy7A10RMMCG5ckwqUciMDB0or2AdKlmsNDH1Xs'
+            b'O2cb00nFR6ar5UdtKSRBnNtRU0w4KcqoOFXWbuw+5qWM/VfKOlR9oemK9v6R6o4C4u2PKtpVq9buM/UNXdvz5oCgZq9M8bagpuMc59'
+            b'e1+xSRj/esGOGsrZQrrlTsVY4IPq1sD58KtKt+eedjSq6a3onQbx3xKL/1xQ3H8dfTOxMfoGEC3EQYbBLYM7ANQlSHBnZFCsnpLgUZ'
+            b'w4l9HfTuYZ4+lHFkkfNQC4scZiReZ/QgayTCjkWZNBziL76Y+kI+xpaE1UKgGD1JCDJcWsbpdtIgtoFe2Oel4Uw1JkmcqRRBhPlGcE'
+            b'4ZQVZqTOp2psLmpMFmXEJ0hbmQHAt02RnmVbuPJT3wUOxUgTeMp8dHEQp9a1ZYEBJs25JuK6pw9zZyqQ/7qDHRLO53qZy+l0pzZvkk'
+            b'l1wQNm0nEhVHBOSIIHStcVu3LQWONh2y2+GlFc9O2IDeu44+t2EH49hbH0R2Uf5Trvz3yYbmoiSMV8LSVreS61Fp2c5Q1U7ty6z181'
+            b'+ciBezxRydPKXLzhYGG95Pe7r5zIonxglZk4g5NFegWzDBnYjHm7i0u7Z+X8PB5mftTleHh8N0Z6Bn4ZqQaPPz1TZBRuaDwtUPrRGJ'
+            b's9bicIzHXuAiO2KzYxkV3WazmSc2e+npTT+xabn2W223gvXfEEpO+fH0b8XzhTk='))
     elif ext == '.mpc':
         d = b'MPCKSH\x0cp\n\xc9\xab\x08\x01\x00\x1b\x0bRG\x0c\x01\x00\x00\x00\x00\x00\x00\x00\x00EI\x07\xa0\x01\x1e' \
             b'\x01SO\x08\x0c\x00\x00\x00\x00AP\x04\x00ST\x06\x01\x12\xb0SE\x03'
@@ -175,6 +257,58 @@ def create_test_file(filename):
             b'\xe3\x9c\xe3\x94\xe3\xdf\xe3\xb5\xe4#\xe3\xe2\xe4f\xe4\x0f\xe4\xb0\xe4O\xe5!\xe4o\xe5\x94\xe4\xa3\xe6' \
             b'\x07\xe4\xbc\xe6X\xe4\xd2\xe6\xc3\xe4\xd6\xe7\x18\xe4\xf1\xe7\x83\xe5\t\xe7\xed\xe5 \xe8_\xe58\xe8' \
             b'\xce\xe5r\xe9C\xe5\xae\xe9\xc9\xe5\xe9\xeao\xe6(\xeb\x06\xe6\x82\xeb\xac'
+    elif ext == '.mkv':
+        # Valid minimal Matroska video container with one subtitle track.
+        d = zlib.decompress(base64.b64decode(
+            'eNqTcr2/eLFTWyOj03cg/tTI4vS5kcOpqSM3saQovzg70akdKNTayCgR3JDOyAACYmaCvrNL9vju7gle3SLquTIteE2TADOEKxay'
+            'LhvErYdwZYJ3g7giTyBcoZDD6SDuvzf++xlGwSgYBaNgFIyCUTAKRsEoGAWjYBSMglEw2AGo3/9d6/rGZn4nB9+GxTmZSalJuTkK'
+            'ZYZ6JnqmCtoKQAHYCAJI0FzPMNxxXm52WW5qUXqqQpmlpZ6Bgoa6U2l2orqmgpmJblJmiUtnh0O/A9h0l8QOboVXD+Rv1zAUL5mQ'
+            'uqVrlo0dg8gX7u6WrPyZ30GDDBvXrb/eyFh8tOPLwTexM7afWtjcKDinkUFp65zm0ryUtu7g+BDXiBD90BA3C6Wtc0Fib1zERqNt'
+            'FIyCUYAK5J23lU583siwoGdhWyOQ75E5u4n5BWjocuruyZsbGbb3fW9k/Ngkcu5DI/MmoAxoFNPRqLjYUS/5QHcyogRKP9HturjZ'
+            'KSDYpb3J0Cz9xALXxR0uoUGOIZ7+fi7tkwwMrEDIUM8ABtJPTHddPMEv1NfJNSje3y3eLcjR1xWou9Ew/cQ018X9CBmnyBCwhFH6'
+            'id2ui6fHB4cATQ0O8XQOjg8P8gzx9HOPdwwIcGknUMKmn9jqungONt0ujiGu8aEhzi7tk40MjMx0DUx1jQ0VDEytTIytDIC2HgY6'
+            'FFlfiKM70D1rgZ5VgHlRAd0jCmjuBwDHXfqa'))
+    elif ext == '.mka':
+        # Valid Matroska audio container with chapters and one attachment.
+        d = zlib.decompress(base64.b64decode(
+            'eNrtmH9MG2UYx1swTEPMYHGDbf64sURQobQFWgoaubZXrFkB2yMgMdYWSiGUstGC/KGGa4FW5sKvOXBDt+WWTTZGdFgcYcQlONfF'
+            'xJhpJNmytHfPe+W3Yy5zLkbn3QjZsizxP/3nPrnnTb7P875P3ucuuTfvs42I0LS2i5Jqb/P2G/WY9ib1uNYXbLR5m5s8DTZtgHd1'
+            'UtJUS7tTKhFI/STZdMh7xTT1keVUx2bjSK1l1JcUvya3kGMNgvxgTT5tmRJkin5NbiJnnIKsWpNJuhO7ebk5fU1uxY/V8/KpxeXS'
+            'oxIREREREREREREREREREZH/H+He//uLs1/6KvtN7bSr3u6wN7qwVoUsV5aHvYTxjvUGguBUyxQV+GeNDa2NjmanA2vVaGRyLCNd'
+            '29JgS38BU+Vm2eu9+g+DReb2e8n1tmAitrTpeNKExHOsN5R6fTLz4HbryWTa9mfKyUWhx3Bx7MIsJfVcCBq55J7Cu6/m+am4tNCw'
+            'v8Vd07UPt5bpTNnGEjJ7l5HcCf6OOHuVJC30qRBl9oU6ijP1ksNUnL2GSlrWPyN+SxERkUci9CSL1LYTRTkGd6dN5m3zGt7p9jra'
+            'vNm7XbZ6t+Gt3jqHy9WE2bxeW3Vdo8PtNYwFD5zVVFXETsUL/c2bxOQqMU1JiKu8XePtXDDFPj5yJVXSOjHi+TY4rf484+O/Omx9'
+            'lGSIjw5R0vb+zk6jm/936t7zO9xO3fs+h3tilJ96uLA/41LanpY+/0Zt0frkgc4uU31NjcvxwOxl7j95N8/pJlrw/DlKcgRXHsUz'
+            'Kd71I+fi2rhKbitXyE2gbdzb6C5KQH+jb+A6ImEF3WEvoj72DMplu9F3TC2qYV5Bq9FEFIxG4eXoGdgQ3Qs/RyogFFHCZCQBLkfi'
+            '4VbkFrsj+itrjl5lD0Svseejc+xydIV9kllkC5g/2EomBTyMAgaZEhhnnPATMwCzzHG4w5wGKfsVPMuehefZaTCwIahiaWhj+2GQ'
+            'bYVx9l34nq2DedYOiVAK24GALHgNMkHHjwVQCG+AEar4sQP2wDDshwiEYCdioRZtRF+gUrSDa0L7ORptiJ1DlbHLaCb2C0qdu4H8'
+            'c09wK3MYZ53P536Yb+Z0C2PcpYUFrmkxI5aw5IuNLh2iXhda431TPeOU5Ovu25T0hm9LwioVL3TIcY3Hg+dVn99bff94c4Z7Cdqv'
+            'LbPoAwFljipfLpc7w0cIOqgvN+OksbREH+iXywvuPTK5XKFSq/PVKmd4kKB7S8pNWsJsLTVYDWbcRPApKIUzPETQPfcj2jdJIeBX'
+            'avhFUwQ9aLWQfF4LadRZrBVmI2ksKbbiZWX6wL+c4s5wiKCHH7Vaj5OEtZzU6QMDSrlSlSXPy8pRYPK8glxVQY7GGZ7ht/rgOhIv'
+            '5nd0mq8ZWy8Se7gU7KEK/gGe/SWt'))
+    elif ext == '.webm':
+        # Minimal EBML header and segment for WebM container recognition tests.
+        d = b'\x1a\x45\xdf\xa3\x9f\x42\x86\x81\x01\x42\xf7\x81\x01\x42\xf2\x81\x04' \
+            b'\x42\xf3\x81\x08\x42\x82\x84\x77\x65\x62\x6d\x42\x87\x81\x04\x42\x85\x81\x02' \
+            b'\x18\x53\x80\x67\xff\xff\xff\xff\xff\xff\xff\xff'
+    elif ext == '.dsf':
+        # Minimal DSF file with valid DSD/fmt/data chunks.
+        d = b'DSD '\
+            b'\x1c\x00\x00\x00\x00\x00\x00\x00' \
+            b'@\x00\x00\x00\x00\x00\x00\x00' \
+            b'\x00\x00\x00\x00\x00\x00\x00\x00' \
+            b'fmt ' \
+            b'4\x00\x00\x00\x00\x00\x00\x00' \
+            b'\x01\x00\x00\x00' \
+            b'\x00\x00\x00\x00' \
+            b'\x02\x00\x00\x00' \
+            b'\x02\x00\x00\x00' \
+            b'\x80\x11+\x00' \
+            b'\x01\x00\x00\x00' \
+            b'\x00\x10\x00\x00' \
+            b'\x00\x00\x00\x00\x00\x00\x00\x00' \
+            b'data' \
+            b'\x0c\x00\x00\x00\x00\x00\x00\x00'
     elif ext == '.jpg':
         d = b'\xff\xd8\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \
             b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \
@@ -196,7 +330,21 @@ def create_test_file(filename):
             b'\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x02' \
             b'\x01\x01?\x10\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 ' \
             b'\xff\xda\x00\x08\x01\x01\x00\x01?\x10\x1f\xff\xd9'
+    elif ext == '.lrc':
+        d = b'[ti:Title]\r\n' \
+            b'\r\n' \
+            b'[00:00:00.000]Intro\r\n' \
+            b'[00:00:20.181]Middle\r\n' \
+            b'[25:34:56.789]Much later\r\n' \
+            b'[25:35:00.999]\r\n'
     else:
         d = b''
     with open(filename, 'wb') as fh:
         fh.write(d)
+
+
+def ignore_audio_properties(s):
+    """Transform output string to ignore exact audio properties."""
+    s = re.sub(r'\d+ kbps', 'n kbps', s)
+    s = re.sub(r'AAC \d+ bit ', '', s)
+    return s
diff --git a/src/test/test_cli_functions.py b/src/test/test_cli_functions.py
index f1ab3964..cac42c61 100755
--- a/src/test/test_cli_functions.py
+++ b/src/test/test_cli_functions.py
@@ -7,7 +7,9 @@ import subprocess
 import tempfile
 import platform
 import json
-from kid3testsupport import kid3_cli_path, call_kid3_cli, create_test_file
+from kid3testsupport import kid3_cli_path, call_kid3_cli, create_test_file, ignore_audio_properties, \
+    Kid3ConfigFileUsingOnlyTagLib, Kid3ConfigFileUsingOnlyId3lib, Kid3ConfigFileUsingOnlyOggFlac, \
+    Kid3ConfigFileUsingOnlyMp4v2
 
 
 def setUpModule():
@@ -39,6 +41,17 @@ class CliFunctionsTestCase(unittest.TestCase):
         self.assertIn(b"Unknown command 'invalid', -h for help.", stderr)
         self.assertEqual(p.returncode, 1)
 
+    def test_invalid_interactive(self):
+        p = subprocess.Popen([kid3_cli_path()],
+                             stdin=subprocess.PIPE,
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE,
+                             universal_newlines=True)
+        stdout, stderr = p.communicate('invalid\n')
+        self.assertEqual(stdout, '')
+        self.assertIn("Unknown command 'invalid'. Type 'help' for help.", stderr)
+        self.assertEqual(p.returncode, 0)
+
     def test_timeout(self):
         self.assertEqual(call_kid3_cli(
             ['-c', 'timeout', '-c', 'timeout 5000', '-c', 'timeout',
@@ -55,29 +68,148 @@ class CliFunctionsTestCase(unittest.TestCase):
     def test_exit(self):
         self.assertEqual(call_kid3_cli(['-c', 'exit']), '')
 
+    def test_config(self):
+        self.assertEqual(call_kid3_cli(['-c', 'config']),
+            'BatchImport\n'
+            'Export\n'
+            'File\n'
+            'FilenameFormat\n'
+            'Filter\n'
+            'Import\n'
+            'Network\n'
+            'NumberTracks\n'
+            'Playlist\n'
+            'RenameFolder\n'
+            'Tag\n'
+            'TagFormat\n')
+        self.assertEqual(call_kid3_cli(
+            ['-c', 'config Tag.id3v2Version',
+             '-c', 'config Tag.id3v2Version 3',
+             '-c', 'config Tag.id3v2Version']),
+            'ID3v2_3_0\n'
+            '3\n'
+            '3\n')
+        self.assertEqual(call_kid3_cli(
+            ['-c', 'config TagFormat.caseConversion',
+             '-c', 'config TagFormat.caseConversion NoChanges',
+             '-c', 'config TagFormat.caseConversion']),
+            'AllFirstLettersUppercase\n'
+            'NoChanges\n'
+            'NoChanges\n')
+        self.assertEqual(call_kid3_cli(
+            ['-c', 'config NumberTracks.numberTracksDestination',
+             '-c', 'config NumberTracks.numberTracksDestination 12',
+             '-c', 'config NumberTracks.numberTracksDestination']),
+            '1\n'
+            '21\n'
+            '21\n')
+
+    def test_config_invalid(self):
+        p = subprocess.Popen([kid3_cli_path(), '-c', 'config NoSuchGroup.foo'],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        stdout, stderr = p.communicate()
+        self.assertEqual(stdout, b'')
+        self.assertIn(b'NoSuchGroup does not exist', stderr)
+        self.assertEqual(p.returncode, 1)
+
+        p = subprocess.Popen([kid3_cli_path(), '-c', 'config Tag.textEncoding UTF8'],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        stdout, stderr = p.communicate()
+        self.assertEqual(stdout, b'')
+        self.assertIn(b'Invalid value', stderr)
+        self.assertEqual(p.returncode, 1)
+
+    def test_config_playlist(self):
+        lines = call_kid3_cli(
+            ['-c', 'config Playlist.format PF_XSPF',
+             '-c', 'config Playlist.format',
+             '-c', 'config Playlist.location PL_EveryDirectory',
+             '-c', 'config Playlist.location']).splitlines()
+        self.assertEqual(lines[0], 'PF_XSPF')
+        self.assertEqual(lines[1], 'PF_XSPF')
+        self.assertEqual(lines[2], 'PL_EveryDirectory')
+        self.assertEqual(lines[3], 'PL_EveryDirectory')
+
+    def test_cd_pwd_and_invalid_cd(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            lines = call_kid3_cli(['-c', 'pwd', '-c', 'cd "%s"' % tmpdir, '-c', 'pwd']).splitlines()
+            self.assertTrue(lines)
+            self.assertEqual(lines[-1], tmpdir)
+
+            p = subprocess.Popen([kid3_cli_path(), '-c', 'cd %s/no_such_dir' % tmpdir],
+                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            stdout, stderr = p.communicate()
+            self.assertEqual(stdout, b'')
+            self.assertIn(b'does not exist', stderr)
+            self.assertEqual(p.returncode, 1)
+
+    def test_filter_invalid_name(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            p = subprocess.Popen([kid3_cli_path(), '-c', 'filter NoSuchFilter', tmpdir],
+                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            stdout, stderr = p.communicate()
+            self.assertEqual(stdout, b'')
+            self.assertIn(b'NoSuchFilter not found.', stderr)
+            self.assertIn(b'Available:', stderr)
+            self.assertEqual(p.returncode, 1)
+
+    def test_select_missing_file(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            p = subprocess.Popen([kid3_cli_path(), '-c', 'select no_such_file.mp3', tmpdir],
+                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            stdout, stderr = p.communicate()
+            self.assertEqual(stdout, b'')
+            self.assertIn(b'no_such_file.mp3 not found', stderr)
+            self.assertEqual(p.returncode, 1)
+
+    def test_usage_error(self):
+        p = subprocess.Popen([kid3_cli_path(), '-c', 'set'],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                             universal_newlines=True)
+        stdout, stderr = p.communicate()
+        self.assertEqual(stdout, 'Usage:\n')
+        self.assertIn('set N V [T]  Set tag frame', stderr)
+        self.assertEqual(p.returncode, 1)
+
     def test_ls(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             self.assertEqual(call_kid3_cli(['-c', 'ls', tmpdir]), '')
             create_test_file(os.path.join(tmpdir, 'test.mp3'))
             self.assertEqual(call_kid3_cli(['-c', 'ls', tmpdir]), '  --- test.mp3\n')
 
-    def test_id3v1(self):
+    def test_id3v1_taglib(self):
+        with Kid3ConfigFileUsingOnlyTagLib():
+            self._run_id3v1_tests()
+
+    def test_id3v1_id3lib(self):
+        with Kid3ConfigFileUsingOnlyId3lib():
+            self._run_id3v1_tests()
+
+    def _run_id3v1_tests(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             mp3path = os.path.join(tmpdir, 'test.mp3')
             create_test_file(mp3path)
+            actual = call_kid3_cli(
+                ['-c', 'get all 1', mp3path])
+            if not actual:
+                # Do not fail if Id3libMetadata plugin is not present
+                return
+            self.assertEqual(actual,
+                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                '  Name: test.mp3\n')
             with open(mp3path, 'rb') as mp3fh:
                 empty_mp3_bytes = mp3fh.read()
             self.assertEqual(call_kid3_cli(
                 ['-c', 'get title 1',
                  '-c', 'set title "A Title" 1',
                  '-c', 'get title 1',
-                 '-c', 'get all 1',
-                 '-c', 'save', mp3path]),
+                 '-c', 'save',
+                 '-c', 'get all 1', mp3path]),
                 'A Title\n'
                 'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
                 '  Name: test.mp3\n'
                 'Tag 1: ID3v1.1\n'
-                '* Title  A Title\n')
+                '  Title  A Title\n')
             self.assertEqual(call_kid3_cli(
                 ['-c', 'set artist "An Artist" 1',
                  '-c', 'set album "An Album" 1',
@@ -151,22 +283,151 @@ class CliFunctionsTestCase(unittest.TestCase):
                 ba = mp3fh.read()
                 self.assertEqual(ba, empty_mp3_bytes)
 
-    def test_id3v2(self):
+    def test_id3v2_taglib(self):
+        with Kid3ConfigFileUsingOnlyTagLib():
+            self._run_id3v2_tests()
+            with tempfile.TemporaryDirectory() as tmpdir:
+                mp3path = os.path.join(tmpdir, 'test.mp3')
+                lrcpath = os.path.join(tmpdir, 'test.lrc')
+                eventspath = os.path.join(tmpdir, 'events.lrc')
+                syltpath = os.path.join(tmpdir, 'sylt.lrc')
+                etcopath = os.path.join(tmpdir, 'etco.lrc')
+                chappath = os.path.join(tmpdir, 'chap.lrc')
+                create_test_file(mp3path)
+                create_test_file(lrcpath)
+                etco_bytes = (
+                        b'[ti:Title]\r\n'
+                        b'\r\n'
+                        b'[00:00:00.000]intro start\r\n'
+                        b'[00:01:02.003]refrain start\r\n'
+                        b'[24:25:26.270]outro end\r\n')
+                with open(eventspath, 'wb') as lrcfh:
+                    lrcfh.write(etco_bytes)
+                with open(lrcpath, 'rb') as lrcfh:
+                    lrc_bytes = lrcfh.read()
+                actual = call_kid3_cli(
+                    ['-c', 'set artist "Artist" 2',
+                     '-c', 'set album "Album" 2',
+                     '-c', 'save',
+                     '-c', 'to24',
+                     '-c', 'get all 2',
+                     '-c', 'to23',
+                     '-c', 'get all 2',
+                     '-c', 'remove 2', mp3path])
+                expected = ('File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                            '  Name: test.mp3\n'
+                            'Tag 2: ID3v2.4.0\n'
+                            '  Artist  Artist\n'
+                            '  Album   Album\n'
+                            'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                            '  Name: test.mp3\n'
+                            'Tag 2: ID3v2.3.0\n'
+                            '  Artist  Artist\n'
+                            '  Album   Album\n')
+                self.assertEqual(actual, expected)
+                self.assertEqual(call_kid3_cli(
+                    ['-c', 'set POPM 5',
+                     '-c', 'get all 2',
+                     '-c', 'get POPM.Email',
+                     '-c', 'get POPM.Rating',
+                     '-c', 'get POPM.Counter',
+                     '-c', 'set POPM.Email [email protected]',
+                     '-c', 'set POPM.Rating 4',
+                     '-c', 'set POPM.Counter 3',
+                     '-c', 'get all 2',
+                     '-c', 'get POPM.Email',
+                     '-c', 'get POPM.Rating',
+                     '-c', 'get POPM.Counter',
+                     '-c', 'get POPM', mp3path]),
+                     'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                     '  Name: test.mp3\n'
+                     'Tag 2: ID3v2.3.0\n'
+                     '* Rating  5\n'
+                     '\n'
+                     '5\n'
+                     '0\n'
+                     'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                     '  Name: test.mp3\n'
+                     'Tag 2: ID3v2.3.0\n'
+                     '* Rating  4\n'
+                     '[email protected]\n'
+                     '4\n'
+                     '3\n'
+                     '4\n')
+                call_kid3_cli(
+                    ['-c', 'remove',
+                     '-c', 'set title Title',
+                     '-c', 'set UFID.Owner http://www.id3.org/test/ufid.html',
+                     '-c', 'set UFID.Identifier 54455354',
+                     '-c', 'set "SYLT:%s" ""' % lrcpath,
+                     '-c', 'set "ETCO:%s" ""' % eventspath,
+                     '-c', 'set "Chapters:%s" ""' % lrcpath,
+                     mp3path])
+                actual = call_kid3_cli(
+                    ['-c', 'get all',
+                     '-c', 'get UFID.Owner',
+                     '-c', 'get UFID.Identifier',
+                     '-c', 'get "SYLT:%s"' % syltpath,
+                     '-c', 'get "ETCO:%s"' % etcopath,
+                     '-c', 'get "Chapters:%s"' % chappath,
+                     mp3path])
+                expected = ('File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                            '  Name: test.mp3\n'
+                            'Tag 2: ID3v2.3.0\n'
+                            '  Title                Title\n'
+                            '  Chapter              chp01\n'
+                            '  Chapter              chp02\n'
+                            '  Chapter              chp03\n'
+                            '  Table of Contents    toc01\n'
+                            '  Chapters             \n'
+                            '  Event Timing Codes   \n'
+                            '  Synchronized Lyrics  \n'
+                            '  File ID: ufid        54455354\n'
+                            'http://www.id3.org/test/ufid.html\n'
+                            '54455354\n'
+                            '\n'
+                            '\n'
+                            '\n')
+                self.assertEqual(actual, expected)
+                with open(syltpath, 'rb') as lrcfh:
+                    ba = lrcfh.read()
+                    self.assertEqual(ba, lrc_bytes)
+                with open(etcopath, 'rb') as lrcfh:
+                    ba = lrcfh.read()
+                    self.assertEqual(ba, etco_bytes)
+                with open(chappath, 'rb') as lrcfh:
+                    ba = lrcfh.read()
+                    self.assertEqual(ba, lrc_bytes)
+
+    def test_id3v2_id3lib(self):
+        with Kid3ConfigFileUsingOnlyId3lib():
+            self._run_id3v2_tests()
+
+    def _run_id3v2_tests(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             mp3path = os.path.join(tmpdir, 'test.mp3')
             jpgpath = os.path.join(tmpdir, 'test.jpg')
             picpath = os.path.join(tmpdir, 'folder.jpg')
+            lyricspath = os.path.join(tmpdir, 'lyrics.txt')
+            usltpath = os.path.join(tmpdir, 'uslt.txt')
             create_test_file(mp3path)
             create_test_file(jpgpath)
+            uslt_bytes = 'Schön\nsind die Lyrics.\n'.encode()
+            with open(lyricspath, 'wb') as txtfh:
+                txtfh.write(uslt_bytes)
             with open(mp3path, 'rb') as mp3fh:
                 empty_mp3_bytes = mp3fh.read()
             with open(jpgpath, 'rb') as jpgfh:
                 jpg_bytes = jpgfh.read()
-            self.assertEqual(call_kid3_cli(
+            actual = call_kid3_cli(
                 ['-c', 'get title 2',
-                 '-c', 'get all 2', mp3path]),
-                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
-                '  Name: test.mp3\n')
+                 '-c', 'get all 2', mp3path])
+            if not actual:
+                # Do not fail if Id3libMetadata plugin is not present
+                return
+            expected = ('File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                        '  Name: test.mp3\n')
+            self.assertEqual(actual, expected)
             self.assertEqual(call_kid3_cli(
                 ['-c', 'set artist "An Artist" 2',
                  '-c', 'set album "An Album" 2',
@@ -202,11 +463,7 @@ class CliFunctionsTestCase(unittest.TestCase):
                  '-c', 'get all',
                  '-c', 'revert',
                  '-c', 'get all',
-                 '-c', 'to24',
-                 '-c', 'get all 2',
-                 '-c', 'to23',
                  '-c', 'get picture:"%s" "A Description" 2' % picpath,
-                 '-c', 'get all 2',
                  '-c', 'remove 2', mp3path]),
                 'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
                 '  Name: test.mp3\n'
@@ -252,68 +509,89 @@ class CliFunctionsTestCase(unittest.TestCase):
                 '  Lyricist                A Lyricist\n'
                 '  Picture: Cover (front)  A Description\n'
                 '  Playlist Delay          100\n'
-                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
-                '  Name: test.mp3\n'
-                'Tag 2: ID3v2.4.0\n'
-                '  Artist                  An Artist\n'
-                '  Album                   An Album\n'
-                '  Comment                 A Comment\n'
-                '  Date                    2016\n'
-                '  Track Number            3\n'
-                '  Genre                   Power Metal\n'
-                '  Lyricist                A Lyricist\n'
-                '  Picture: Cover (front)  A Description\n'
-                '  Playlist Delay          100\n'
-                'A Description\n'
-                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
-                '  Name: test.mp3\n'
-                'Tag 2: ID3v2.3.0\n'
-                '  Artist                  An Artist\n'
-                '  Album                   An Album\n'
-                '  Comment                 A Comment\n'
-                '  Date                    2016\n'
-                '  Track Number            3\n'
-                '  Genre                   Power Metal\n'
-                '  Lyricist                A Lyricist\n'
-                '  Picture: Cover (front)  A Description\n'
-                '  Playlist Delay          100\n')
+                'A Description\n')
             with open(mp3path, 'rb') as mp3fh:
                 ba = mp3fh.read()
                 self.assertEqual(ba, empty_mp3_bytes)
             with open(picpath, 'rb') as jpgfh:
                 ba = jpgfh.read()
                 self.assertEqual(ba, jpg_bytes)
-            self.assertEqual(call_kid3_cli(
-                ['-c', 'set POPM 5',
-                 '-c', 'get all 2',
-                 '-c', 'get POPM.Email',
-                 '-c', 'get POPM.Rating',
-                 '-c', 'get POPM.Counter',
-                 '-c', 'set POPM.Email [email protected]',
-                 '-c', 'set POPM.Rating 4',
-                 '-c', 'set POPM.Counter 3',
-                 '-c', 'get all 2',
-                 '-c', 'get POPM.Email',
-                 '-c', 'get POPM.Rating',
-                 '-c', 'get POPM.Counter',
-                 '-c', 'get POPM', mp3path]),
-                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
-                '  Name: test.mp3\n'
-                'Tag 2: ID3v2.3.0\n'
-                '* Rating  5\n'
-                '\n'
-                '5\n'
-                '0\n'
-                'File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
-                '  Name: test.mp3\n'
-                'Tag 2: ID3v2.3.0\n'
-                '* Rating  4\n'
-                '[email protected]\n'
-                '4\n'
-                '3\n'
-                '4\n')
+            actual = call_kid3_cli(
+                ['-c', 'remove',
+                 '-c', 'set Rating 196',
+                 '-c', 'set Description Description',
+                 '-c', 'set PRIV.Owner WM/WMContentID',
+                 '-c', 'set PRIV.Data 177bf099-f5f8-4e93-b083-f12befddd4a4',
+                 '-c', 'set Track 1/2',
+                 '-c', 'set CATALOGNUMBER "Catalog Number"',
+                 '-c', 'set RELEASECOUNTRY "Release Country"',
+                 '-c', 'set GROUPING Grouping',
+                 '-c', 'set TOPE "Schön 1|Schön 2"',
+                 '-c', 'set "USLT:%s" ""' % lyricspath,
+                 '-c', 'save',
+                 '-c', 'get all',
+                 '-c', 'get PRIV.Owner',
+                 '-c', 'get PRIV.Data',
+                 '-c', 'get "USLT:%s"' % usltpath,
+                 mp3path])
+            expected = ('File: MPEG 1 Layer 3 64 kbps 44100 Hz 1 Channels\n'
+                        '  Name: test.mp3\n'
+                        'Tag 2: ID3v2.3.0\n'
+                        '  Track Number     1/2\n'
+                        '  Catalog Number   Catalog Number\n'
+                        '  Grouping         Grouping\n'
+                        '  Lyrics           Schön\n'
+                        'sind die Lyrics.\n'
+                        '\n'
+                        '  Original Artist  Schön 1|Schön 2\n'
+                        '  Description      Description\n'
+                        '  Release Country  Release Country\n'
+                        '  Rating           196\n'
+                        'WM/WMContentID\n'
+                        '177bf099-f5f8-4e93-b083-f12befddd4a4\n'
+                        'Schön\n'
+                        'sind die Lyrics.\n'
+                        '\n')
+            self.assertEqual(actual, expected)
+            with open(usltpath, 'rb') as txtfh:
+                ba = txtfh.read()
+                self.assertEqual(ba, uslt_bytes)
+
+    def test_flac_taglib(self):
+        with Kid3ConfigFileUsingOnlyTagLib():
+            self._run_flac_tests()
+            with tempfile.TemporaryDirectory() as tmpdir:
+                flacpath = os.path.join(tmpdir, 'test.flac')
+                jpgpath = os.path.join(tmpdir, 'test.jpg')
+                create_test_file(flacpath)
+                create_test_file(jpgpath)
+                actual = call_kid3_cli(
+                    ['-c', 'remove 123',
+                    '-c', 'set artist "Artist 1" 1',
+                    '-c', 'set artist "Artist 2" 2',
+                    '-c', 'set artist "Artist 3" 3',
+                    '-c', 'set CUSTOM "Custom 2" 2',
+                    '-c', 'set CUSTOM "Custom 3" 3',
+                    '-c', 'set picture:"%s" "Picture 3 Description" 3' % jpgpath,
+                    '-c', 'get all 123', flacpath])
+                expected = ('File: FLAC 16 bit 4234 kbps 44100 Hz 1 Channels\n'
+                            '  Name: test.flac\n'
+                            'Tag 1: ID3v1.1\n'
+                            '* Artist  Artist 1\n'
+                            'Tag 2: Vorbis\n'
+                            '* Artist  Artist 2\n'
+                            '* CUSTOM  Custom 2\n'
+                            'Tag 3: ID3v2.4.0\n'
+                            '* Artist                  Artist 3\n'
+                            '* Picture: Cover (front)  Picture 3 Description\n'
+                            '  CUSTOM                  Custom 3\n')
+                self.assertEqual(actual, expected)
 
-    def test_flac(self):
+    def test_flac_oggflac(self):
+        with Kid3ConfigFileUsingOnlyOggFlac():
+            self._run_flac_tests()
+
+    def _run_flac_tests(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             flacpath = os.path.join(tmpdir, 'test.flac')
             jpgpath = os.path.join(tmpdir, 'test.jpg')
@@ -400,6 +678,293 @@ class CliFunctionsTestCase(unittest.TestCase):
                 ba = jpgfh.read()
                 self.assertEqual(ba, jpg_bytes)
 
+    def test_ogg_taglib(self):
+        with Kid3ConfigFileUsingOnlyTagLib():
+            self._run_ogg_tests()
+
+    def test_ogg_oggflac(self):
+        with Kid3ConfigFileUsingOnlyOggFlac():
+            self._run_ogg_tests()
+
+    def _run_ogg_tests(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            path = os.path.join(tmpdir, 'test.ogg')
+            jpgpath = os.path.join(tmpdir, 'test.jpg')
+            picpath = os.path.join(tmpdir, 'folder.jpg')
+            create_test_file(path)
+            create_test_file(jpgpath)
+            with open(jpgpath, 'rb') as jpgfh:
+                jpg_bytes = jpgfh.read()
+            actual = ignore_audio_properties(call_kid3_cli(
+                ['-c', 'get all 2',
+                 '-c', 'set artist "A first artist"',
+                 '-c', 'set artist[1] "A second artist"',
+                 '-c', 'set album "Album Name"',
+                 '-c', 'set track 4',
+                 '-c', 'set genre "Heavy Metal"',
+                 '-c', 'set date 2017',
+                 '-c', 'set comment[0] "Comment 1"',
+                 '-c', 'set comment[1] "Comment 2"',
+                 '-c', 'set comment[2] "Comment 3"',
+                 '-c', 'set Lyricist "A Lyricist" 2',
+                 '-c', 'set tracktotal 12',
+                 '-c', 'set "disc number" 1',
+                 '-c', 'set DISCTOTAL 2',
+                 '-c', 'set picture:"%s" "Picture Description"' % jpgpath,
+                 '-c', 'get comment[1]',
+                 '-c', 'get comment',
+                 '-c', 'get', path]))
+            expected = ignore_audio_properties('File: Ogg Vorbis 112 kbps 44100 Hz 2 Channels 0:01\n'
+                        '  Name: test.ogg\n'
+                        'Tag 2: Vorbis\n'
+                        '  Encoder  Lavc61.19.101 libvorbis\n'
+                        'Comment 2\n'
+                        'Comment 1\n'
+                        'File: Ogg Vorbis 112 kbps 44100 Hz 2 Channels 0:01\n'
+                        '  Name: test.ogg\n'
+                        'Tag 2: Vorbis\n'
+                        '* Artist                  A first artist\n'
+                        '* Artist                  A second artist\n'
+                        '* Album                   Album Name\n'
+                        '* Comment                 Comment 1\n'
+                        '* Comment                 Comment 2\n'
+                        '* Comment                 Comment 3\n'
+                        '* Date                    2017\n'
+                        '* Track Number            4\n'
+                        '* Genre                   Heavy Metal\n'
+                        '* Disc Number             1\n'
+                        '* Lyricist                A Lyricist\n'
+                        '* Picture: Cover (front)  Picture Description\n'
+                        '* Total Discs             2\n'
+                        '  Encoder                 Lavc61.19.101 libvorbis\n'
+                        '* Total Tracks            12\n')
+            self.assertEqual(actual, expected)
+            actual = ignore_audio_properties(call_kid3_cli(
+                ['-c', 'set artist[1] ""',
+                 '-c', 'set comment[1] "Comment B"',
+                 '-c', 'set comment[0] "Comment A"',
+                 '-c', 'set comment[2] "Comment C"',
+                 '-c', 'set lyricist ""',
+                 '-c', 'set picture[1]:"%s" "Back Cover"' % jpgpath,
+                 '-c', 'set picture[1].picturetype 4',
+                 '-c', 'set picture[0].description "Front Cover"',
+                 '-c', 'get picture[1]:"%s"' % picpath,
+                 '-c', 'get',
+                 '-c', 'remove',
+                 '-c', 'get', path]))
+            expected = ignore_audio_properties('Back Cover\n'
+                        'File: Ogg Vorbis 112 kbps 44100 Hz 2 Channels 0:01\n'
+                        '  Name: test.ogg\n'
+                        'Tag 2: Vorbis\n'
+                        '* Artist                  A first artist\n'
+                        '  Album                   Album Name\n'
+                        '* Comment                 Comment A\n'
+                        '* Comment                 Comment B\n'
+                        '* Comment                 Comment C\n'
+                        '  Date                    2017\n'
+                        '  Track Number            4\n'
+                        '  Genre                   Heavy Metal\n'
+                        '  Disc Number             1\n'
+                        '* Picture: Cover (front)  Front Cover\n'
+                        '* Picture: Cover (back)   Back Cover\n'
+                        '  Total Discs             2\n'
+                        '  Encoder                 Lavc61.19.101 libvorbis\n'
+                        '  Total Tracks            12\n'
+                        'File: Ogg Vorbis 112 kbps 44100 Hz 2 Channels 0:01\n'
+                        '  Name: test.ogg\n')
+            self.assertEqual(actual, expected)
+            with open(picpath, 'rb') as jpgfh:
+                ba = jpgfh.read()
+                self.assertEqual(ba, jpg_bytes)
+
+    def test_mp4_taglib(self):
+        with Kid3ConfigFileUsingOnlyTagLib():
+            self._run_mp4_tests()
+
+    def test_mp4_mp4v2(self):
+        with Kid3ConfigFileUsingOnlyMp4v2():
+            self._run_mp4_tests()
+
+    def _run_mp4_tests(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            path = os.path.join(tmpdir, 'test.m4a')
+            jpgpath = os.path.join(tmpdir, 'test.jpg')
+            picpath = os.path.join(tmpdir, 'folder.jpg')
+            lrcpath = os.path.join(tmpdir, 'chapters.lrc')
+            chpneropath = os.path.join(tmpdir, 'chpnero.lrc')
+            chpqtpath = os.path.join(tmpdir, 'chpqt.lrc')
+            create_test_file(path)
+            create_test_file(jpgpath)
+            create_test_file(lrcpath)
+            with open(lrcpath, 'rb') as lrcfh:
+                lrc_bytes = lrcfh.read()
+            with open(jpgpath, 'rb') as jpgfh:
+                jpg_bytes = jpgfh.read()
+            actual = call_kid3_cli(
+                ['-c', 'get all 2', path])
+            if not actual:
+                # Do not fail if Mp4v2Metadata plugin is not present
+                return
+            expected = ignore_audio_properties(
+                'File: MP4 AAC 16 bit 9 kbps 44100 Hz 2 Channels\n'
+                '  Name: test.m4a\n')
+            self.assertEqual(ignore_audio_properties(actual), expected)
+            actual = ignore_audio_properties(call_kid3_cli(
+                ['-c', 'set "Title" "Title"',
+                 '-c', 'set "Artist" "Artist"',
+                 '-c', 'set "Album" "Album"',
+                 '-c', 'set "Comment" "Comment"',
+                 '-c', 'set "Date" "2026"',
+                 '-c', 'set "Track Number" "3/4"',
+                 '-c', 'set "Genre" "Genre"',
+                 '-c', 'set "Album Artist" "Album Artist"',
+                 '-c', 'set "Author" "Author"',
+                 '-c', 'set "BPM" "120"',
+                 '-c', 'set "Compilation" "1"',
+                 '-c', 'set "Composer" "Composer"',
+                 '-c', 'set "Copyright" "Copyright"',
+                 '-c', 'set "Description" "Description"',
+                 '-c', 'set "Disc Number" "1/2"',
+                 '-c', 'set "Encoded-by" "Encoded-by"',
+                 '-c', 'set "Encoder Settings" "Encoder Settings"',
+                 '-c', 'set "Grouping" "Grouping"',
+                 '-c', 'set "Lyrics" "Lyrics"',
+                 '-c', 'set "Rating" "Rating"',
+                 '-c', 'set "Sort Album" "Sort Album"',
+                 '-c', 'set "Sort Album Artist" "Sort Album Artist"',
+                 '-c', 'set "Sort Artist" "Sort Artist"',
+                 '-c', 'set "Sort Composer" "Sort Composer"',
+                 '-c', 'set "Sort Name" "Sort Name"',
+                 '-c', 'set "Work" "Work"',
+                 '-c', 'set "pgap" "1"',
+                 '-c', 'set "akID" "23"',
+                 '-c', 'set "apID" "Purchase Account"',
+                 '-c', 'set "atID" "67"',
+                 '-c', 'set "catg" "Category"',
+                 '-c', 'set "cnID" "32"',
+                 '-c', 'set "geID" "31"',
+                 '-c', 'set "hdvd" "1"',
+                 '-c', 'set "keyw" "Keywords"',
+                 '-c', 'set "ldes" "Long Description"',
+                 '-c', 'set "pcst" "1"',
+                 '-c', 'set "plID" "1234567890"',
+                 '-c', 'set "purd" "Purchase Date"',
+                 '-c', 'set "rtng" "23"',
+                 '-c', 'set "sfID" "7"',
+                 '-c', 'set "sosn" "Sort TV Show"',
+                 '-c', 'set "stik" "23"',
+                 '-c', 'set "tven" "TV Episode Number"',
+                 '-c', 'set "tves" "89"',
+                 '-c', 'set "tvnn" "TV Network"',
+                 '-c', 'set "tvsh" "Show"',
+                 '-c', 'set "tvsn" "99"',
+                 '-c', 'set "purl" "purl"',
+                 '-c', 'set "egid" "egid"',
+                 '-c', 'set "cmID" "33"',
+                 '-c', 'set "xid " "xid "',
+                 '-c', 'save',
+                 '-c', 'get all 2', path]))
+            expected = ignore_audio_properties(
+                'File: MP4 9 kbps 44100 Hz 2 Channels\n'
+                '  Name: test.m4a\n'
+                'Tag 2: MP4\n'
+                '  Title              Title\n'
+                '  Artist             Artist\n'
+                '  Album              Album\n'
+                '  Comment            Comment\n'
+                '  Date               2026\n'
+                '  Track Number       3/4\n'
+                '  Genre              Genre\n'
+                '  Album Artist       Album Artist\n'
+                '  Author             Author\n'
+                '  BPM                120\n'
+                '  Compilation        1\n'
+                '  Composer           Composer\n'
+                '  Copyright          Copyright\n'
+                '  Disc Number        1/2\n'
+                '  Encoded-by         Encoded-by\n'
+                '  Encoder Settings   Encoder Settings\n'
+                '  Grouping           Grouping\n'
+                '  Lyrics             Lyrics\n'
+                '  Description        Description\n'
+                '  Sort Album         Sort Album\n'
+                '  Sort Album Artist  Sort Album Artist\n'
+                '  Sort Artist        Sort Artist\n'
+                '  Sort Composer      Sort Composer\n'
+                '  Sort Name          Sort Name\n'
+                '  Rating             Rating\n'
+                '  Work               Work\n'
+                '  Account Type       23\n'
+                '  Purchase Account   Purchase Account\n'
+                '  Artist ID          67\n'
+                '  Category           Category\n'
+                '  Composer ID        33\n'
+                '  Catalog ID         32\n'
+                '  Podcast GUID       egid\n'
+                '  Genre ID           31\n'
+                '  HD Video           1\n'
+                '  Keyword            Keywords\n'
+                '  Long Description   Long Description\n'
+                '  Podcast            1\n'
+                '  Gapless Playback   1\n'
+                '  Album ID           1234567890\n'
+                '  Purchase Date      Purchase Date\n'
+                '  Podcast URL        purl\n'
+                '  Rating/Advisory    23\n'
+                '  Country Code       7\n'
+                '  Sort Show          Sort TV Show\n'
+                '  Media Type         23\n'
+                '  TV Episode         TV Episode Number\n'
+                '  TV Episode Number  89\n'
+                '  TV Network Name    TV Network\n'
+                '  TV Show Name       Show\n'
+                '  TV Season          99\n'
+                '  XID                xid \n')
+            self.assertEqual(actual, expected)
+            # Called separately because libmp4v2 prints
+            # ReadChildAtoms: "/tmp/tmp08j22ihf/test.m4a": In atom meta missing child atom ilst
+            call_kid3_cli(['-c', 'remove', '-c', 'set title Title', path])
+            call_kid3_cli(
+                ['-c', 'set picture:"%s" ""' % jpgpath,
+                 '-c', 'set "Chapters (Nero):%s" ""' % lrcpath,
+                 '-c', 'set "Chapters (QT):%s" ""' % lrcpath,
+                 path])
+            actual = ignore_audio_properties(call_kid3_cli(
+                ['-c', 'get all',
+                 '-c', 'get picture:"%s"' % picpath,
+                 '-c', 'get "Chapters (Nero):%s"' % chpneropath,
+                 '-c', 'get "Chapters (QT):%s"' % chpqtpath,
+                 path]))
+            expected = ignore_audio_properties(
+                'File: MP4 AAC 16 bit 9 kbps 44100 Hz 2 Channels\n'
+                '  Name: test.m4a\n'
+                'Tag 2: MP4\n'
+                '  Title                   Title\n'
+                '  Picture: Cover (front)  \n'
+                '  Chapters (Nero)         \n'
+                '  Chapters (QT)           \n'
+                '\n\n\n')
+            if 'Chapters (Nero)' not in actual:
+                # Using a TagLib version which does not support MP4 chapters
+                self.assertEqual(actual, ignore_audio_properties(
+                    'File: MP4 n kbps 44100 Hz 2 Channels\n'
+                    '  Name: test.m4a\n'
+                    'Tag 2: MP4\n'
+                    '  Title                   Title\n'
+                    '  Picture: Cover (front)  \n'
+                    '\n'))
+                return
+            self.assertEqual(actual, expected)
+            with open(picpath, 'rb') as jpgfh:
+                ba = jpgfh.read()
+                self.assertEqual(ba, jpg_bytes)
+            with open(chpneropath, 'rb') as lrcfh:
+                ba = lrcfh.read()
+                self.assertEqual(ba, lrc_bytes)
+            with open(chpqtpath, 'rb') as lrcfh:
+                ba = lrcfh.read()
+                self.assertEqual(ba, lrc_bytes)
+
     def test_frame_selection(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             test1path = os.path.join(tmpdir, 'test1.mp3')
@@ -738,6 +1303,47 @@ class CliFunctionsTestCase(unittest.TestCase):
                  '-c', 'select all',
                  '-c', 'get', tmpdir]),
                 expected)
+            actual = call_kid3_cli(
+                ['-c', 'select all',
+                 '-c', 'set Date "" 2',
+                 '-c', 'set Artist "" 1',
+                 '-c', 'set Album "" 2',
+                 '-c', 'get',
+                 '-c', 'revert',
+                 '-c', 'get',
+                 tmpdir])
+            expected = ('Tag 1:\n'
+                        '  Title         ≠\n'
+                        '  Album         Kings Of Metal\n'
+                        '  Date          1988\n'
+                        '  Track Number  ≠\n'
+                        '  Genre         Metal\n'
+                        'Tag 2:\n'
+                        '  Title               ≠\n'
+                        '  Artist              Manowar\n'
+                        '  Track Number        ≠\n'
+                        '  Genre               Metal\n'
+                        '  Copyright           ≠\n'
+                        '  Encoder Settings    ≠\n'
+                        '  Rating Information  ≠\n'
+                        'Tag 1:\n'
+                        '  Title         ≠\n'
+                        '  Artist        Manowar\n'
+                        '  Album         Kings Of Metal\n'
+                        '  Date          1988\n'
+                        '  Track Number  ≠\n'
+                        '  Genre         Metal\n'
+                        'Tag 2:\n'
+                        '  Title               ≠\n'
+                        '  Artist              Manowar\n'
+                        '  Album               Kings Of Metal\n'
+                        '  Date                1988\n'
+                        '  Track Number        ≠\n'
+                        '  Genre               Metal\n'
+                        '  Copyright           ≠\n'
+                        '  Encoder Settings    ≠\n'
+                        '  Rating Information  ≠\n')
+            self.assertEqual(actual, expected)
             call_kid3_cli(['-c', 'fromtag "%{track} %{title}" 2', os.path.join(tmpdir, '*.*')])
             self.assertRegex(call_kid3_cli(
                 ['-c', 'ls', tmpdir]),
@@ -799,6 +1405,250 @@ class CliFunctionsTestCase(unittest.TestCase):
                 '09 The Warriors Prayer.aif\n'
                 '10 Blood Of The Kings.wma\n')
 
+    def test_ape_picture_and_custom_frame(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            apepath = os.path.join(tmpdir, 'test.ape')
+            jpgpath = os.path.join(tmpdir, 'cover.jpg')
+            create_test_file(apepath)
+            create_test_file(jpgpath)
+            self.assertEqual(call_kid3_cli(
+                ['-c', 'set picture:"%s" "FrontCover" 2' % jpgpath,
+                 '-c', 'get all 2',
+                 '-c', 'set picture "" 2',
+                 '-c', 'get all 2',
+                 '-c', 'set !MY_CUSTOM "XValue" 2',
+                 '-c', 'get all 2',
+                 apepath]),
+                'File: APE 3.990 16 bit 44100 Hz 1 Channels\n'
+                '  Name: test.ape\n'
+                'Tag 2: APE\n'
+                '* Picture: Cover (front)  FrontCover\n'
+                'File: APE 3.990 16 bit 44100 Hz 1 Channels\n'
+                '  Name: test.ape\n'
+                'File: APE 3.990 16 bit 44100 Hz 1 Channels\n'
+                '  Name: test.ape\n'
+                'Tag 2: APE\n'
+                '* MY_CUSTOM  XValue\n')
+
+    def test_wavpack_and_mpc_ape_tags(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            wavpack_path = os.path.join(tmpdir, 'test.wv')
+            mpc_path = os.path.join(tmpdir, 'test.mpc')
+            create_test_file(wavpack_path)
+            create_test_file(mpc_path)
+            self.assertEqual(call_kid3_cli(
+                ['-c', 'set title "WvTitle" 2',
+                 '-c', 'get all 2', wavpack_path]),
+                'File: WavPack 407 16 bit 44100 Hz 1 Channels\n'
+                '  Name: test.wv\n'
+                'Tag 2: APE\n'
+                '* Title  WvTitle\n')
+            self.assertEqual(call_kid3_cli(
+                ['-c', 'set title "MpcTitle" 2',
+                 '-c', 'get all 2', mpc_path]),
+                'File: MPC 44100 Hz 1 Channels\n'
+                '  Name: test.mpc\n'
+                'Tag 2: APE\n'
+                '* Title  MpcTitle\n')
+
+    def test_asf_and_wma_tag_edit_commands(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            path = os.path.join(tmpdir, 'test.wma')
+            jpgpath = os.path.join(tmpdir, 'cover.jpg')
+            create_test_file(path)
+            create_test_file(jpgpath)
+            expected = ('File: ASF 128 kbps 44100 Hz 1 Channels\n'
+                        '  Name: test.wma\n'
+                        'Tag 2: ASF\n'
+                        '* Title               AsfTitle\n'
+                        '* Artist              AsfArtist\n'
+                        '  Comment             \n'
+                        '  Copyright           \n'
+                        '  Encoder Settings    Lavf61.7.102\n'
+                        '  Rating Information  \n')
+            actual = call_kid3_cli(
+                ['-c', 'set title "AsfTitle" 2',
+                 '-c', 'set artist "AsfArtist" 2',
+                 '-c', 'get all 2', path])
+            self.assertEqual(actual, expected)
+            actual = call_kid3_cli(
+                ['-c', 'set CUSTOM "Custom Value"',
+                 '-c', 'set picture:"%s" "CoverAsf"' % jpgpath,
+                 '-c', 'set copyright "2026 Artist"',
+                 '-c', 'set comment "A comment"',
+                 '-c', 'set AverageLevel 1234',
+                 '-c', 'set WM/MediaClassPrimaryID 6c1adf28-040e-425e-a90b-df1f2f6262f0',
+                 '-c', 'get all 2',
+                 '-c', 'remove',
+                 '-c', 'get all 2', path])
+            # Do not set rating, it does not behave consistently with older versions
+            expected = ('File: ASF 128 kbps 44100 Hz 1 Channels\n'
+                        '  Name: test.wma\n'
+                        'Tag 2: ASF\n'
+                        '  Title                   AsfTitle\n'
+                        '  Artist                  AsfArtist\n'
+                        '* Comment                 A comment\n'
+                        '* Copyright               2026 Artist\n'
+                        '  Encoder Settings        Lavf61.7.102\n'
+                        '* Picture: Cover (front)  CoverAsf\n'
+                        '* AverageLevel            1234\n'
+                        '* CUSTOM                  Custom Value\n'
+                        '  Rating Information      \n'
+                        '* WM/MediaClassPrimaryID  6C1ADF28-040E-425E-A90B-DF1F2F6262F0\n'
+                        'File: ASF 128 kbps 44100 Hz 1 Channels\n'
+                        '  Name: test.wma\n'
+                        'Tag 2:\n'
+                        '  Title               \n'
+                        '  Artist              \n'
+                        '* Comment             \n'
+                        '* Copyright           \n'
+                        '  Rating Information  \n')
+            self.assertEqual(actual, expected)
+
+
+    def test_dsf_tags_and_picture(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            dsfpath = os.path.join(tmpdir, 'test.dsf')
+            jpgpath = os.path.join(tmpdir, 'cover.jpg')
+            create_test_file(dsfpath)
+            create_test_file(jpgpath)
+            out = call_kid3_cli(
+                ['-c', 'set title "DsfTitle" 2',
+                 '-c', 'set artist "DsfArtist" 2',
+                 '-c', 'set album "DsfAlbum" 2',
+                 '-c', 'set date 2026 2',
+                 '-c', 'set track 7 2',
+                 '-c', 'set genre Metal 2',
+                 '-c', 'set picture:"%s" "CoverDsf" 2' % jpgpath,
+                 '-c', 'get all 2', dsfpath])
+            self.assertRegex(out,
+                'File: DSF 1(?: 5645 kbps 2822(?:400|528) Hz 2 Channels)?\\n'
+                '  Name: test\\.dsf\\n'
+                'Tag 2: ID3v2\\.[34]\\.0\\n'
+                '\\* Title\\s+DsfTitle\\n'
+                '\\* Artist\\s+DsfArtist\\n'
+                '\\* Album\\s+DsfAlbum\\n'
+                '\\* Date\\s+2026\\n'
+                '\\* Track Number\\s+7(?:/1)?\\n'
+                '\\* Genre\\s+Metal\\n'
+                '\\* Picture: Cover \\(front\\)\\s+CoverDsf\\n')
+
+    def test_matroska(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            path = os.path.join(tmpdir, 'test.mkv')
+            create_test_file(path)
+            actual = call_kid3_cli(
+                ['-c', 'set title "MatroskaTitle" 2',
+                 '-c', 'set artist "MatroskaArtist" 2',
+                 '-c', 'set album "MatroskaAlbum" 2',
+                 '-c', 'get all 2', path])
+            if not actual:
+                # Do not fail if running tests with TagLib not supporting Matroska
+                return
+            expected = ("File: Matroska Version 4 45 kbps 0:01\n"
+                        "  Name: test.mkv\n"
+                        "Tag 2: Matroska\n"
+                        "* Title                         MatroskaTitle\n"
+                        "* Artist                        MatroskaArtist\n"
+                        "* Album                         MatroskaAlbum\n"
+                        "  BPS                           16\n"
+                        "  Duration                      00:00:01.000000000\n"
+                        "  NUMBER_OF_BYTES               2\n"
+                        "  NUMBER_OF_FRAMES              1\n"
+                        "  _STATISTICS_TAGS              BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES\n"
+                        "  _STATISTICS_WRITING_APP       mkvmerge v99.0 ('Buka') 64-bit\n"
+                        "  _STATISTICS_WRITING_DATE_UTC  2026-05-31 05:43:02\n")
+            self.assertEqual(actual, expected)
+
+            path = os.path.join(tmpdir, 'test.mka')
+            jpgpath = os.path.join(tmpdir, 'test.jpg')
+            picpath = os.path.join(tmpdir, 'folder.jpg')
+            lrcpath = os.path.join(tmpdir, 'chapters.lrc')
+            chppath = os.path.join(tmpdir, 'chp.lrc')
+            create_test_file(path)
+            create_test_file(jpgpath)
+            create_test_file(lrcpath)
+            with open(lrcpath, 'rb') as lrcfh:
+                lrc_bytes = lrcfh.read()
+            with open(jpgpath, 'rb') as jpgfh:
+                jpg_bytes = jpgfh.read()
+            actual = call_kid3_cli(
+                ['-c', 'set title "MatroskaTitle" 2',
+                 '-c', 'set artist "MatroskaArtist" 2',
+                 '-c', 'set album "MatroskaAlbum" 2',
+                 '-c', 'get all 2', path])
+            expected = ('File: Matroska Version 4 Codec A_PCM/INT/LIT 44100 Hz 2 Channels\n'
+                        '  Name: test.mka\n'
+                        'Tag 2: Matroska\n'
+                        '* Title                         MatroskaTitle\n'
+                        '* Artist                        MatroskaArtist\n'
+                        '* Album                         MatroskaAlbum\n'
+                        '  BPS                           2368000\n'
+                        '  Chapters                      {"default":false}\n'
+                        '  Duration                      00:00:00.001677876\n'
+                        '  General Object                \n'
+                        '  NUMBER_OF_BYTES               296\n'
+                        '  NUMBER_OF_FRAMES              1\n'
+                        '  _STATISTICS_TAGS              BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES\n'
+                        '  _STATISTICS_WRITING_APP       mkvmerge v99.0 (\'Buka\') 64-bit\n'
+                        '  _STATISTICS_WRITING_DATE_UTC  2026-05-31 05:46:39\n')
+            self.assertEqual(actual, expected)
+
+            call_kid3_cli(
+                ['-c', 'set Title Title',
+                 '-c', 'set Artist ""',
+                 '-c', 'set Album ""',
+                 '-c', 'set _STATISTICS_TAGS ""',
+                 '-c', 'set _STATISTICS_WRITING_APP ""',
+                 '-c', 'set _STATISTICS_WRITING_DATE_UTC ""',
+                 '-c', 'set NUMBER_OF_BYTES ""',
+                 '-c', 'set NUMBER_OF_FRAMES 3',
+                 '-c', 'set "General Object.Description" "ObjDesc"',
+                 '-c', 'set "Chapters:%s" ""' % lrcpath,
+                 '-c', 'set Picture:"%s" ""' % jpgpath,
+                 path])
+            actual = call_kid3_cli(
+                ['-c', 'get',
+                 '-c', 'get "Chapters:%s"' % chppath,
+                 '-c', 'get Picture:"%s"' % picpath,
+                 '-c', 'get Duration.TargetType',
+                 '-c', 'get Duration.Language',
+                 '-c', 'get Duration.Default',
+                 '-c', 'get Duration.TrackId',
+                 '-c', 'get Duration.Text',
+                 path])
+            expected = ('File: Matroska Version 4 Codec A_PCM/INT/LIT 44100 Hz 2 Channels\n'
+                        '  Name: test.mka\n'
+                        'Tag 2: Matroska\n'
+                        '  Title                   Title\n'
+                        '  Picture: Cover (front)  \n'
+                        '  BPS                     2368000\n'
+                        '  Chapters                \n'
+                        '  Duration                00:00:00.001677876\n'
+                        '  General Object          ObjDesc\n'
+                        '  NUMBER_OF_FRAMES        3\n'
+                        '\n'
+                        '\n'
+                        '0\n'
+                        'und\n'
+                        'true\n'
+                        '5324681441320058677\n'
+                        '00:00:00.001677876\n')
+            self.assertEqual(actual, expected)
+            with open(chppath, 'rb') as lrcfh:
+                ba = lrcfh.read()
+                self.assertEqual(ba, lrc_bytes)
+            with open(picpath, 'rb') as jpgfh:
+                ba = jpgfh.read()
+                self.assertEqual(ba, jpg_bytes)
+            actual = call_kid3_cli(
+                ['-c', 'remove',
+                 '-c', 'get',
+                 path])
+            expected = ('File: Matroska Version 4 Codec A_PCM/INT/LIT 44100 Hz 2 Channels\n'
+                        '  Name: test.mka\n')
+            self.assertEqual(actual, expected)
+
     def test_filename_tag_format(self):
         with tempfile.TemporaryDirectory() as tmpdir:
             albumdir = os.path.join(tmpdir, 'An Artist - 2016 - An Album')
@@ -892,6 +1742,18 @@ class CliFunctionsJsonTestCase(unittest.TestCase):
             self.assertEqual(result, rsp)
             self.assertEqual(p.returncode, 1)
 
+    def test_invalid_interactive(self):
+        p = subprocess.Popen([kid3_cli_path()],
+                             stdin=subprocess.PIPE,
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE,
+                             universal_newlines=True)
+        stdout, stderr = p.communicate('{"method":"unknown"}\n')
+        self.assertEqual(json.loads(stdout),
+                         {'error': {'code': -32601,
+                                    'message': "Unknown command 'unknown'"}})
+        self.assertEqual(p.returncode, 0)
+
     def test_timeout(self):
         self.assertEqual(call_kid3_cli(
             ['-c', '{"method":"timeout"}',
@@ -909,6 +1771,63 @@ class CliFunctionsJsonTestCase(unittest.TestCase):
             '{"result":{"timeout":"default"}}\n'
             '{"result":{"timeout":"default"}}\n')
 
+    def test_config(self):
+        self.assertEqual(call_kid3_cli(['-c', '{"method":"config"}']),
+            '{"result":["BatchImport","Export","File","FilenameFormat",'
+            '"Filter","Import","Network","NumberTracks","Playlist",'
+            '"RenameFolder","Tag","TagFormat"]}\n')
+        self.assertEqual(call_kid3_cli(
+            ['-c', '{"method":"config","params":["Tag.id3v2Version"]}',
+             '-c', '{"method":"config","params":["Tag.id3v2Version","4"]}',
+             '-c', '{"method":"config","params":["Tag.id3v2Version"]}']),
+            '{"result":"ID3v2_3_0"}\n'
+            '{"result":"4"}\n'
+            '{"result":"4"}\n')
+
+        p = subprocess.Popen([kid3_cli_path(), '-c',
+                              '{"method":"config","params":["Tag.textEncoding","UTF8"]}'],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                             universal_newlines=True)
+        stdout, stderr = p.communicate()
+        self.assertNotIn('Invalid value', stderr)
+        self.assertEqual(json.loads(stdout),
+                         {'error': {'code': -1, 'message': 'Invalid value '}})
+        self.assertEqual(p.returncode, 1)
+
+    def test_config_playlist(self):
+        lines = call_kid3_cli(
+            ['-c', '{"method":"config","params":["Playlist.format","PF_XSPF"]}',
+             '-c', '{"method":"config","params":["Playlist.format"]}',
+             '-c', '{"method":"config","params":["Playlist.location","PL_EveryDirectory"]}',
+             '-c', '{"method":"config","params":["Playlist.location"]}']).splitlines()
+        self.assertEqual(lines[0], '{"result":"PF_XSPF"}')
+        self.assertEqual(lines[1], '{"result":"PF_XSPF"}')
+        self.assertEqual(lines[2], '{"result":"PL_EveryDirectory"}')
+        self.assertEqual(lines[3], '{"result":"PL_EveryDirectory"}')
+
+    def test_select_missing_file(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            p = subprocess.Popen([kid3_cli_path(), '-c',
+                                  '{"method":"select","params":["no_such_file.mp3"]}',
+                                  tmpdir],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                             universal_newlines=True)
+            stdout, stderr = p.communicate()
+            self.assertEqual(json.loads(stdout),
+                             {'error': {'code': -1,
+                                        'message': 'no_such_file.mp3 not found'}})
+            self.assertEqual(p.returncode, 1)
+
+    def test_usage_error(self):
+        p = subprocess.Popen([kid3_cli_path(), '-c', '{"method":"set"}'],
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                             universal_newlines=True)
+        stdout, stderr = p.communicate()
+        self.assertNotIn('Usage: set N V [T]  Set tag frame', stderr)
+        self.assertEqual(stdout,
+            '{"error":{"code":-32600,"message":"Usage: set N V [T]  Set tag frame"}}\n')
+        self.assertEqual(p.returncode, 1)
+
     def test_json_methods(self):
         self.maxDiff = None
         with tempfile.TemporaryDirectory() as tmpdir:
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.