SickGear/lib/hachoir/parser/audio/id3.py
2023-02-09 13:41:15 +00:00

531 lines
16 KiB
Python

"""
ID3 metadata parser, supported versions: 1.O, 2.2, 2.3 and 2.4
Informations: http://www.id3.org/
Author: Victor Stinner
"""
from hachoir.field import (FieldSet, MatchError, ParserError,
Enum, UInt8, UInt24, UInt32,
CString, String, RawBytes,
Bit, Bits, NullBytes, NullBits)
from hachoir.core.text_handler import textHandler
from hachoir.core.tools import humanDuration
from hachoir.core.endian import NETWORK_ENDIAN
from functools import reduce
class ID3v1(FieldSet):
static_size = 128 * 8
GENRE_NAME = {
0: "Blues",
1: "Classic Rock",
2: "Country",
3: "Dance",
4: "Disco",
5: "Funk",
6: "Grunge",
7: "Hip-Hop",
8: "Jazz",
9: "Metal",
10: "New Age",
11: "Oldies",
12: "Other",
13: "Pop",
14: "R&B",
15: "Rap",
16: "Reggae",
17: "Rock",
18: "Techno",
19: "Industrial",
20: "Alternative",
21: "Ska",
22: "Death Metal",
23: "Pranks",
24: "Soundtrack",
25: "Euro-Techno",
26: "Ambient",
27: "Trip-Hop",
28: "Vocal",
29: "Jazz+Funk",
30: "Fusion",
31: "Trance",
32: "Classical",
33: "Instrumental",
34: "Acid",
35: "House",
36: "Game",
37: "Sound Clip",
38: "Gospel",
39: "Noise",
40: "AlternRock",
41: "Bass",
42: "Soul",
43: "Punk",
44: "Space",
45: "Meditative",
46: "Instrumental Pop",
47: "Instrumental Rock",
48: "Ethnic",
49: "Gothic",
50: "Darkwave",
51: "Techno-Industrial",
52: "Electronic",
53: "Pop-Folk",
54: "Eurodance",
55: "Dream",
56: "Southern Rock",
57: "Comedy",
58: "Cult",
59: "Gangsta",
60: "Top 40",
61: "Christian Rap",
62: "Pop/Funk",
63: "Jungle",
64: "Native American",
65: "Cabaret",
66: "New Wave",
67: "Psychadelic",
68: "Rave",
69: "Showtunes",
70: "Trailer",
71: "Lo-Fi",
72: "Tribal",
73: "Acid Punk",
74: "Acid Jazz",
75: "Polka",
76: "Retro",
77: "Musical",
78: "Rock & Roll",
79: "Hard Rock",
# Following are winamp extentions
80: "Folk",
81: "Folk-Rock",
82: "National Folk",
83: "Swing",
84: "Fast Fusion",
85: "Bebob",
86: "Latin",
87: "Revival",
88: "Celtic",
89: "Bluegrass",
90: "Avantgarde",
91: "Gothic Rock",
92: "Progressive Rock",
93: "Psychedelic Rock",
94: "Symphonic Rock",
95: "Slow Rock",
96: "Big Band",
97: "Chorus",
98: "Easy Listening",
99: "Acoustic",
100: "Humour",
101: "Speech",
102: "Chanson",
103: "Opera",
104: "Chamber Music",
105: "Sonata",
106: "Symphony",
107: "Booty Bass",
108: "Primus",
109: "Porn Groove",
110: "Satire",
111: "Slow Jam",
112: "Club",
113: "Tango",
114: "Samba",
115: "Folklore",
116: "Ballad",
117: "Power Ballad",
118: "Rhythmic Soul",
119: "Freestyle",
120: "Duet",
121: "Punk Rock",
122: "Drum Solo",
123: "A capella",
124: "Euro-House",
125: "Dance Hall",
126: "Goa",
127: "Drum & Bass",
128: "Club-House",
129: "Hardcore",
130: "Terror",
131: "Indie",
132: "Britpop",
133: "Negerpunk",
134: "Polsk Punk",
135: "Beat",
136: "Christian Gangsta Rap",
137: "Heavy Metal",
138: "Black Metal",
139: "Crossover",
140: "Contemporary Christian",
141: "Christian Rock ",
142: "Merengue",
143: "Salsa",
144: "Trash Metal",
145: "Anime",
146: "JPop",
147: "Synthpop"
}
def createFields(self):
yield String(self, "signature", 3, "IDv1 signature (\"TAG\")", charset="ASCII")
if self["signature"].value != "TAG":
raise MatchError(
"Stream doesn't look like ID3v1 (wrong signature)!")
# TODO: Charset of below strings?
yield String(self, "song", 30, "Song title", strip=" \0", charset="ISO-8859-1")
yield String(self, "author", 30, "Author", strip=" \0", charset="ISO-8859-1")
yield String(self, "album", 30, "Album title", strip=" \0", charset="ISO-8859-1")
yield String(self, "year", 4, "Year", strip=" \0", charset="ISO-8859-1")
# TODO: Write better algorithm to guess ID3v1 version
version = self.getVersion()
if version in ("v1.1", "v1.1b"):
if version == "v1.1b":
# ID3 v1.1b
yield String(self, "comment", 29, "Comment", strip=" \0", charset="ISO-8859-1")
yield UInt8(self, "track_nb", "Track number")
else:
# ID3 v1.1
yield String(self, "comment", 30, "Comment", strip=" \0", charset="ISO-8859-1")
yield Enum(UInt8(self, "genre", "Genre"), self.GENRE_NAME)
else:
# ID3 v1.0
yield String(self, "comment", 31, "Comment", strip=" \0", charset="ISO-8859-1")
def getVersion(self):
addr = self.absolute_address + 126 * 8
bytes = self.stream.readBytes(addr, 2)
# last byte (127) is not space?
if bytes[1] != 32:
# byte 126 is nul?
if bytes[0] == 0:
return "v1.1"
else:
return "v1.1b"
else:
return "1.0"
def createDescription(self):
version = self.getVersion()
return "ID3 %s: author=%s, song=%s" % (
version, self["author"].value, self["song"].value)
def getCharset(field):
try:
key = field.value
return ID3_StringCharset.charset_name[key]
except KeyError:
raise ParserError("ID3v2: Invalid charset (%s)." % key)
class ID3_String(FieldSet):
STRIP = " \0"
def createFields(self):
yield String(self, "text", self._size // 8, "Text", charset="ISO-8859-1", strip=self.STRIP)
class ID3_StringCharset(ID3_String):
STRIP = " \0"
charset_desc = {
0: "ISO-8859-1",
1: "UTF-16 with BOM",
2: "UTF-16 (big endian)",
3: "UTF-8"
}
charset_name = {
0: "ISO-8859-1",
1: "UTF-16",
2: "UTF-16-BE",
3: "UTF-8"
}
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
size = (self.size - self.current_size) // 8
if not size:
return
charset = getCharset(self["charset"])
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_GEOB(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
charset = getCharset(self["charset"])
yield CString(self, "mime", "MIME type", charset=charset)
yield CString(self, "filename", "File name", charset=charset)
yield CString(self, "description", "Content description", charset=charset)
size = (self.size - self.current_size) // 8
if not size:
return
yield String(self, "text", size, "Text", charset=charset)
class ID3_Comment(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
yield String(self, "lang", 3, "Language", charset="ASCII")
charset = getCharset(self["charset"])
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
size = (self.size - self.current_size) // 8
if not size:
return
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_StringTitle(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
if self.current_size == self.size:
return
charset = getCharset(self["charset"])
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
size = (self.size - self.current_size) // 8
if not size:
return
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_Private(FieldSet):
def createFields(self):
size = self._size // 8
# TODO: Strings charset?
if self.stream.readBytes(self.absolute_address, 9) == b"PeakValue":
yield String(self, "text", 9, "Text")
size -= 9
yield String(self, "content", size, "Content")
class ID3_TrackLength(FieldSet):
def createFields(self):
yield NullBytes(self, "zero", 1)
yield textHandler(String(self, "length", self._size // 8 - 1,
"Length in ms", charset="ASCII"), self.computeLength)
def computeLength(self, field):
try:
ms = int(field.value)
return humanDuration(ms)
except Exception:
return field.value
class ID3_Picture23(FieldSet):
pict_type_name = {
0x00: "Other",
0x01: "32x32 pixels 'file icon' (PNG only)",
0x02: "Other file icon",
0x03: "Cover (front)",
0x04: "Cover (back)",
0x05: "Leaflet page",
0x06: "Media (e.g. lable side of CD)",
0x07: "Lead artist/lead performer/soloist",
0x08: "Artist/performer",
0x09: "Conductor",
0x0A: "Band/Orchestra",
0x0B: "Composer",
0x0C: "Lyricist/text writer",
0x0D: "Recording Location",
0x0E: "During recording",
0x0F: "During performance",
0x10: "Movie/video screen capture",
0x11: "A bright coloured fish",
0x12: "Illustration",
0x13: "Band/artist logotype",
0x14: "Publisher/Studio logotype"
}
def createFields(self):
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
charset = getCharset(self["charset"])
yield String(self, "img_fmt", 3, charset="ASCII")
yield Enum(UInt8(self, "pict_type"), self.pict_type_name)
yield CString(self, "text", "Text", charset=charset, strip=" \0")
size = (self._size - self._current_size) // 8
if size:
yield RawBytes(self, "img_data", size)
class ID3_Picture24(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
charset = getCharset(self["charset"])
yield CString(self, "mime", "MIME type", charset=charset)
yield Enum(UInt8(self, "pict_type"), ID3_Picture23.pict_type_name)
yield CString(self, "description", charset=charset)
size = (self._size - self._current_size) // 8
if size:
yield RawBytes(self, "img_data", size)
class ID3_Chunk(FieldSet):
endian = NETWORK_ENDIAN
tag22_name = {
"TT2": "Track title",
"TP1": "Artist",
"TRK": "Track number",
"COM": "Comment",
"TCM": "Composer",
"TAL": "Album",
"TYE": "Year",
"TEN": "Encoder",
"TCO": "Content type",
"PIC": "Picture"
}
tag23_name = {
"COMM": "Comment",
"GEOB": "Encapsulated object",
"PRIV": "Private",
"TPE1": "Artist",
"TCOP": "Copyright",
"TALB": "Album",
"TENC": "Encoder",
"TYER": "Year",
"TSSE": "Encoder settings",
"TCOM": "Composer",
"TRCK": "Track number",
"PCNT": "Play counter",
"TCON": "Content type",
"TLEN": "Track length",
"TIT2": "Track title",
"WXXX": "User defined URL"
}
handler = {
"COMM": ID3_Comment,
"COM": ID3_Comment,
"GEOB": ID3_GEOB,
"PIC": ID3_Picture23,
"APIC": ID3_Picture24,
"PRIV": ID3_Private,
"TXXX": ID3_StringTitle,
"WOAR": ID3_String,
"WXXX": ID3_StringTitle,
}
def __init__(self, *args):
FieldSet.__init__(self, *args)
if 3 <= self["../ver_major"].value:
self._size = (10 + self["size"].value) * 8
else:
self._size = (self["size"].value + 6) * 8
def createFields(self):
if 3 <= self["../ver_major"].value:
# ID3 v2.3 and 2.4
yield Enum(String(self, "tag", 4, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag23_name)
if 4 <= self["../ver_major"].value:
yield ID3_Size(self, "size") # ID3 v2.4
else:
yield UInt32(self, "size") # ID3 v2.3
yield Bit(self, "tag_alter", "Tag alter preservation")
yield Bit(self, "file_alter", "Tag alter preservation")
yield Bit(self, "rd_only", "Read only?")
yield NullBits(self, "padding[]", 5)
yield Bit(self, "compressed", "Frame is compressed?")
yield Bit(self, "encrypted", "Frame is encrypted?")
yield Bit(self, "group", "Grouping identity")
yield NullBits(self, "padding[]", 5)
size = self["size"].value
is_compressed = self["compressed"].value
else:
# ID3 v2.2
yield Enum(String(self, "tag", 3, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag22_name)
yield UInt24(self, "size")
size = self["size"].value - self.current_size // 8 + 6
is_compressed = False
if size:
cls = None
if not(is_compressed):
tag = self["tag"].value
if tag in ID3_Chunk.handler:
cls = ID3_Chunk.handler[tag]
elif tag[0] == "T":
cls = ID3_StringCharset
if cls:
yield cls(self, "content", "Content", size=size * 8)
else:
yield RawBytes(self, "content", size, "Raw data content")
def createDescription(self):
if self["size"].value != 0:
return "ID3 Chunk: %s" % self["tag"].display
else:
return "ID3 Chunk: (terminator)"
class ID3_Size(Bits):
static_size = 32
def __init__(self, parent, name, description=None):
Bits.__init__(self, parent, name, 32, description)
def createValue(self):
data = self.parent.stream.readBytes(self.absolute_address, 4)
# TODO: Check that bit #7 of each byte is nul: not(ord(data[i]) & 127)
return reduce(lambda x, y: x * 128 + y, (item for item in data))
class ID3v2(FieldSet):
endian = NETWORK_ENDIAN
VALID_MAJOR_VERSIONS = (2, 3, 4)
def __init__(self, parent, name, size=None):
FieldSet.__init__(self, parent, name, size=size)
if not self._size:
self._size = (self["size"].value + 10) * 8
def createDescription(self):
return "ID3 v2.%s.%s" % \
(self["ver_major"].value, self["ver_minor"].value)
def createFields(self):
# Signature + version
yield String(self, "header", 3, "Header (ID3)", charset="ASCII")
yield UInt8(self, "ver_major", "Version (major)")
yield UInt8(self, "ver_minor", "Version (minor)")
# Check format
if self["header"].value != "ID3":
raise MatchError("Signature error, should be \"ID3\".")
if self["ver_major"].value not in self.VALID_MAJOR_VERSIONS \
or self["ver_minor"].value != 0:
raise MatchError(
"Unknown ID3 metadata version (2.%u.%u)"
% (self["ver_major"].value, self["ver_minor"].value))
# Flags
yield Bit(self, "unsync", "Unsynchronisation is used?")
yield Bit(self, "ext", "Extended header is used?")
yield Bit(self, "exp", "Experimental indicator")
yield NullBits(self, "padding[]", 5)
# Size
yield ID3_Size(self, "size")
# All tags
while self.current_size < self._size:
field = ID3_Chunk(self, "field[]")
yield field
if field["size"].value == 0:
break
# Search first byte of the MPEG file
padding = self.seekBit(self._size)
if padding:
yield padding