Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wave: Read metadata from RIFF INFO chunk #538

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions mutagen/wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from mutagen import StreamInfo, FileType

from mutagen.id3 import ID3
from mutagen.id3 import Encoding, ID3, TIT2, TPE1, TORY, TCON, TALB, TRCK
from mutagen._riff import RiffFile, InvalidChunk
from mutagen._iff import error as IffError
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
Expand All @@ -26,6 +26,15 @@

__all__ = ["WAVE", "Open", "delete"]

_INFO_TO_ID3_MAP = {
"INAM": TIT2, # Title
"IART": TPE1, # Artist
"ICRD": TORY, # Original release year
"IGNR": TCON, # Genre
"IPRD": TALB, # Album name
"IPRT": TRCK, # Track
}


class error(IffError):
"""WAVE stream parsing errors."""
Expand Down Expand Up @@ -151,10 +160,38 @@ def delete(self, filething):
def delete(filething):
"""Completely removes the ID3 chunk from the RIFF/WAVE file"""

try:
_WaveFile(filething.fileobj).delete_chunk(u'id3')
except KeyError:
pass
for chunk in [u'id3', u'LIST']:
try:
_WaveFile(filething.fileobj).delete_chunk(chunk)
except KeyError:
pass


def _read_tags_from_riff_list(fileobj):
"""Read metadata tags from RIFF LIST tag and convert to ID3 format."""
wave_file = _WaveFile(fileobj)

if u"LIST" not in wave_file:
return None

list_chunk = wave_file[u"LIST"]
if list_chunk.name != u"INFO":
return None

wave_id3 = _WaveID3()
for chunk in list_chunk.subchunks():
try:
# Value is null-terminated and supposed to be ASCII, but not everyone
# respects that and puts unicode in there anyway
value = chunk.read()[:-1].decode("utf-8")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utf-8 is not a safe choice either, though. foobar2k supports utf-8 in RIFF INFO. But Windows not, and will use Windows-1252 instead (or probably whatever encoding the localized version of Windows is using by default).

In the end it's a mess of course, because there is no way to indicate encoding. But maybe trying UTF-8 first and on UnicodeDecodeError retry with Windows-1252.

except UnicodeDecodeError:
continue
else:
id3_tag = _INFO_TO_ID3_MAP.get(chunk.id)
if id3_tag:
wave_id3.add(id3_tag(encoding=Encoding.UTF8, text=value))

return wave_id3


class WAVE(FileType):
Expand Down Expand Up @@ -199,11 +236,11 @@ def load(self, filething, **kwargs):
try:
self.tags = _WaveID3(fileobj, **kwargs)
except ID3NoHeaderError:
self.tags = None
self.tags = _read_tags_from_riff_list(fileobj)
except ID3Error as e:
raise error(e)
else:
self.tags.filename = self.filename

if self.tags:
self.tags.filename = self.filename

Open = WAVE
Binary file added tests/data/silence-2s-PCM-16000-08-INFO.wav
Binary file not shown.
14 changes: 14 additions & 0 deletions tests/test_wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def setUp(self):
self.tmp_wav_pcm_2s_16000_08_ID3v23 = \
WAVE(self.tmp_fn_pcm_2s_16000_08_ID3v23)

fn_wav_pcm_2s_16000_08_info = \
os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-INFO.wav")
self.wav_pcm_2s_16000_08_INFO = \
WAVE(fn_wav_pcm_2s_16000_08_info)

self.fn_wav_pcm_2s_16000_08_notags = \
os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-notags.wav")
self.wav_pcm_2s_16000_08_notags = \
Expand Down Expand Up @@ -90,6 +95,15 @@ def test_id3_tags_uppercase_chunk(self):
self.assertEquals(id3["TIT2"], "Silence")
self.assertEquals(id3["TPE1"], ["piman / jzig"])

def test_riff_info_tags(self):
id3 = self.wav_pcm_2s_16000_08_INFO.tags
self.assertEquals(id3["TALB"], "Quod Libet Test Data")
self.assertEquals(id3["TCON"], "Silence")
self.assertEquals(id3["TIT2"], "Silence")
self.assertEquals(id3["TPE1"], ["piman, jzig"])
self.assertEquals(id3["TORY"], "2004")
self.assertEquals(id3["TRCK"], ["02"])

def test_delete(self):
self.tmp_wav_pcm_2s_16000_08_ID3v23.delete()

Expand Down