[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: