mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 00:43:37 +00:00
0d9fbc1ad7
This version of SickBeard uses both TVDB and TVRage to search and gather it's series data from allowing you to now have access to and download shows that you couldn't before because of being locked into only what TheTVDB had to offer. Also this edition is based off the code we used in our XEM editon so it does come with scene numbering support as well as all the other features our XEM edition has to offer. Please before using this with your existing database (sickbeard.db) please make a backup copy of it and delete any other database files such as cache.db and failed.db if present, we HIGHLY recommend starting out with no database files at all to make this a fresh start but the choice is at your own risk! Enjoy!
507 lines
16 KiB
Python
507 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 lib.hachoir_core.field import (FieldSet, MatchError, ParserError,
|
|
Enum, UInt8, UInt24, UInt32,
|
|
CString, String, RawBytes,
|
|
Bit, Bits, NullBytes, NullBits)
|
|
from lib.hachoir_core.text_handler import textHandler
|
|
from lib.hachoir_core.tools import humanDuration
|
|
from lib.hachoir_core.endian import NETWORK_ENDIAN
|
|
|
|
class ID3v1(FieldSet):
|
|
static_size = 128 * 8
|
|
GENRE_NAME = {
|
|
0: u"Blues",
|
|
1: u"Classic Rock",
|
|
2: u"Country",
|
|
3: u"Dance",
|
|
4: u"Disco",
|
|
5: u"Funk",
|
|
6: u"Grunge",
|
|
7: u"Hip-Hop",
|
|
8: u"Jazz",
|
|
9: u"Metal",
|
|
10: u"New Age",
|
|
11: u"Oldies",
|
|
12: u"Other",
|
|
13: u"Pop",
|
|
14: u"R&B",
|
|
15: u"Rap",
|
|
16: u"Reggae",
|
|
17: u"Rock",
|
|
18: u"Techno",
|
|
19: u"Industrial",
|
|
20: u"Alternative",
|
|
21: u"Ska",
|
|
22: u"Death Metal",
|
|
23: u"Pranks",
|
|
24: u"Soundtrack",
|
|
25: u"Euro-Techno",
|
|
26: u"Ambient",
|
|
27: u"Trip-Hop",
|
|
28: u"Vocal",
|
|
29: u"Jazz+Funk",
|
|
30: u"Fusion",
|
|
31: u"Trance",
|
|
32: u"Classical",
|
|
33: u"Instrumental",
|
|
34: u"Acid",
|
|
35: u"House",
|
|
36: u"Game",
|
|
37: u"Sound Clip",
|
|
38: u"Gospel",
|
|
39: u"Noise",
|
|
40: u"AlternRock",
|
|
41: u"Bass",
|
|
42: u"Soul",
|
|
43: u"Punk",
|
|
44: u"Space",
|
|
45: u"Meditative",
|
|
46: u"Instrumental Pop",
|
|
47: u"Instrumental Rock",
|
|
48: u"Ethnic",
|
|
49: u"Gothic",
|
|
50: u"Darkwave",
|
|
51: u"Techno-Industrial",
|
|
52: u"Electronic",
|
|
53: u"Pop-Folk",
|
|
54: u"Eurodance",
|
|
55: u"Dream",
|
|
56: u"Southern Rock",
|
|
57: u"Comedy",
|
|
58: u"Cult",
|
|
59: u"Gangsta",
|
|
60: u"Top 40",
|
|
61: u"Christian Rap",
|
|
62: u"Pop/Funk",
|
|
63: u"Jungle",
|
|
64: u"Native American",
|
|
65: u"Cabaret",
|
|
66: u"New Wave",
|
|
67: u"Psychadelic",
|
|
68: u"Rave",
|
|
69: u"Showtunes",
|
|
70: u"Trailer",
|
|
71: u"Lo-Fi",
|
|
72: u"Tribal",
|
|
73: u"Acid Punk",
|
|
74: u"Acid Jazz",
|
|
75: u"Polka",
|
|
76: u"Retro",
|
|
77: u"Musical",
|
|
78: u"Rock & Roll",
|
|
79: u"Hard Rock",
|
|
# Following are winamp extentions
|
|
80: u"Folk",
|
|
81: u"Folk-Rock",
|
|
82: u"National Folk",
|
|
83: u"Swing",
|
|
84: u"Fast Fusion",
|
|
85: u"Bebob",
|
|
86: u"Latin",
|
|
87: u"Revival",
|
|
88: u"Celtic",
|
|
89: u"Bluegrass",
|
|
90: u"Avantgarde",
|
|
91: u"Gothic Rock",
|
|
92: u"Progressive Rock",
|
|
93: u"Psychedelic Rock",
|
|
94: u"Symphonic Rock",
|
|
95: u"Slow Rock",
|
|
96: u"Big Band",
|
|
97: u"Chorus",
|
|
98: u"Easy Listening",
|
|
99: u"Acoustic",
|
|
100: u"Humour",
|
|
101: u"Speech",
|
|
102: u"Chanson",
|
|
103: u"Opera",
|
|
104: u"Chamber Music",
|
|
105: u"Sonata",
|
|
106: u"Symphony",
|
|
107: u"Booty Bass",
|
|
108: u"Primus",
|
|
109: u"Porn Groove",
|
|
110: u"Satire",
|
|
111: u"Slow Jam",
|
|
112: u"Club",
|
|
113: u"Tango",
|
|
114: u"Samba",
|
|
115: u"Folklore",
|
|
116: u"Ballad",
|
|
117: u"Power Ballad",
|
|
118: u"Rhythmic Soul",
|
|
119: u"Freestyle",
|
|
120: u"Duet",
|
|
121: u"Punk Rock",
|
|
122: u"Drum Solo",
|
|
123: u"A capella",
|
|
124: u"Euro-House",
|
|
125: u"Dance Hall",
|
|
126: u"Goa",
|
|
127: u"Drum & Bass",
|
|
128: u"Club-House",
|
|
129: u"Hardcore",
|
|
130: u"Terror",
|
|
131: u"Indie",
|
|
132: u"Britpop",
|
|
133: u"Negerpunk",
|
|
134: u"Polsk Punk",
|
|
135: u"Beat",
|
|
136: u"Christian Gangsta Rap",
|
|
137: u"Heavy Metal",
|
|
138: u"Black Metal",
|
|
139: u"Crossover",
|
|
140: u"Contemporary Christian",
|
|
141: u"Christian Rock ",
|
|
142: u"Merengue",
|
|
143: u"Salsa",
|
|
144: u"Trash Metal",
|
|
145: u"Anime",
|
|
146: u"JPop",
|
|
147: u"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] != ' ':
|
|
# byte 126 is nul?
|
|
if bytes[0] == 0x00:
|
|
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) == "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:
|
|
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, (ord(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
|
|
|