mirror of
https://github.com/moparisthebest/SickRage
synced 2024-11-17 14:55:07 -05: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!
362 lines
12 KiB
Python
362 lines
12 KiB
Python
"""
|
|
EXIF metadata parser (can be found in a JPEG picture for example)
|
|
|
|
Author: Victor Stinner
|
|
"""
|
|
|
|
from lib.hachoir_core.field import (FieldSet, ParserError,
|
|
UInt8, UInt16, UInt32,
|
|
Int32, Enum, String,
|
|
Bytes, SubFile,
|
|
NullBytes, createPaddingField)
|
|
from lib.hachoir_core.endian import LITTLE_ENDIAN, BIG_ENDIAN, NETWORK_ENDIAN
|
|
from lib.hachoir_core.text_handler import textHandler, hexadecimal
|
|
from lib.hachoir_core.tools import createDict
|
|
|
|
MAX_COUNT = 1000
|
|
|
|
def rationalFactory(class_name, size, field_class):
|
|
class Rational(FieldSet):
|
|
static_size = size
|
|
|
|
def createFields(self):
|
|
yield field_class(self, "numerator")
|
|
yield field_class(self, "denominator")
|
|
|
|
def createValue(self):
|
|
return float(self["numerator"].value) / self["denominator"].value
|
|
cls = Rational
|
|
cls.__name__ = class_name
|
|
return cls
|
|
|
|
RationalInt32 = rationalFactory("RationalInt32", 64, Int32)
|
|
RationalUInt32 = rationalFactory("RationalUInt32", 64, UInt32)
|
|
|
|
class BasicIFDEntry(FieldSet):
|
|
TYPE_BYTE = 0
|
|
TYPE_UNDEFINED = 7
|
|
TYPE_RATIONAL = 5
|
|
TYPE_SIGNED_RATIONAL = 10
|
|
TYPE_INFO = {
|
|
1: (UInt8, "BYTE (8 bits)"),
|
|
2: (String, "ASCII (8 bits)"),
|
|
3: (UInt16, "SHORT (16 bits)"),
|
|
4: (UInt32, "LONG (32 bits)"),
|
|
5: (RationalUInt32, "RATIONAL (2x LONG, 64 bits)"),
|
|
7: (Bytes, "UNDEFINED (8 bits)"),
|
|
9: (Int32, "SIGNED LONG (32 bits)"),
|
|
10: (RationalInt32, "SRATIONAL (2x SIGNED LONGs, 64 bits)"),
|
|
}
|
|
ENTRY_FORMAT = createDict(TYPE_INFO, 0)
|
|
TYPE_NAME = createDict(TYPE_INFO, 1)
|
|
|
|
def createFields(self):
|
|
yield Enum(textHandler(UInt16(self, "tag", "Tag"), hexadecimal), self.TAG_NAME)
|
|
yield Enum(textHandler(UInt16(self, "type", "Type"), hexadecimal), self.TYPE_NAME)
|
|
yield UInt32(self, "count", "Count")
|
|
if self["type"].value not in (self.TYPE_BYTE, self.TYPE_UNDEFINED) \
|
|
and MAX_COUNT < self["count"].value:
|
|
raise ParserError("EXIF: Invalid count value (%s)" % self["count"].value)
|
|
value_size, array_size = self.getSizes()
|
|
|
|
# Get offset/value
|
|
if not value_size:
|
|
yield NullBytes(self, "padding", 4)
|
|
elif value_size <= 32:
|
|
if 1 < array_size:
|
|
name = "value[]"
|
|
else:
|
|
name = "value"
|
|
kw = {}
|
|
cls = self.value_cls
|
|
if cls is String:
|
|
args = (self, name, value_size/8, "Value")
|
|
kw["strip"] = " \0"
|
|
kw["charset"] = "ISO-8859-1"
|
|
elif cls is Bytes:
|
|
args = (self, name, value_size/8, "Value")
|
|
else:
|
|
args = (self, name, "Value")
|
|
for index in xrange(array_size):
|
|
yield cls(*args, **kw)
|
|
|
|
size = array_size * value_size
|
|
if size < 32:
|
|
yield NullBytes(self, "padding", (32-size)//8)
|
|
else:
|
|
yield UInt32(self, "offset", "Value offset")
|
|
|
|
def getSizes(self):
|
|
"""
|
|
Returns (value_size, array_size): value_size in bits and
|
|
array_size in number of items.
|
|
"""
|
|
# Create format
|
|
self.value_cls = self.ENTRY_FORMAT.get(self["type"].value, Bytes)
|
|
|
|
# Set size
|
|
count = self["count"].value
|
|
if self.value_cls in (String, Bytes):
|
|
return 8 * count, 1
|
|
else:
|
|
return self.value_cls.static_size * count, count
|
|
|
|
class ExifEntry(BasicIFDEntry):
|
|
OFFSET_JPEG_SOI = 0x0201
|
|
EXIF_IFD_POINTER = 0x8769
|
|
|
|
TAG_WIDTH = 0xA002
|
|
TAG_HEIGHT = 0xA003
|
|
|
|
TAG_GPS_LATITUDE_REF = 0x0001
|
|
TAG_GPS_LATITUDE = 0x0002
|
|
TAG_GPS_LONGITUDE_REF = 0x0003
|
|
TAG_GPS_LONGITUDE = 0x0004
|
|
TAG_GPS_ALTITUDE_REF = 0x0005
|
|
TAG_GPS_ALTITUDE = 0x0006
|
|
TAG_GPS_TIMESTAMP = 0x0007
|
|
TAG_GPS_DATESTAMP = 0x001d
|
|
|
|
TAG_IMG_TITLE = 0x010e
|
|
TAG_FILE_TIMESTAMP = 0x0132
|
|
TAG_SOFTWARE = 0x0131
|
|
TAG_CAMERA_MODEL = 0x0110
|
|
TAG_CAMERA_MANUFACTURER = 0x010f
|
|
TAG_ORIENTATION = 0x0112
|
|
TAG_EXPOSURE = 0x829A
|
|
TAG_FOCAL = 0x829D
|
|
TAG_BRIGHTNESS = 0x9203
|
|
TAG_APERTURE = 0x9205
|
|
TAG_USER_COMMENT = 0x9286
|
|
|
|
TAG_NAME = {
|
|
# GPS
|
|
0x0000: "GPS version ID",
|
|
0x0001: "GPS latitude ref",
|
|
0x0002: "GPS latitude",
|
|
0x0003: "GPS longitude ref",
|
|
0x0004: "GPS longitude",
|
|
0x0005: "GPS altitude ref",
|
|
0x0006: "GPS altitude",
|
|
0x0007: "GPS timestamp",
|
|
0x0008: "GPS satellites",
|
|
0x0009: "GPS status",
|
|
0x000a: "GPS measure mode",
|
|
0x000b: "GPS DOP",
|
|
0x000c: "GPS speed ref",
|
|
0x000d: "GPS speed",
|
|
0x000e: "GPS track ref",
|
|
0x000f: "GPS track",
|
|
0x0010: "GPS img direction ref",
|
|
0x0011: "GPS img direction",
|
|
0x0012: "GPS map datum",
|
|
0x0013: "GPS dest latitude ref",
|
|
0x0014: "GPS dest latitude",
|
|
0x0015: "GPS dest longitude ref",
|
|
0x0016: "GPS dest longitude",
|
|
0x0017: "GPS dest bearing ref",
|
|
0x0018: "GPS dest bearing",
|
|
0x0019: "GPS dest distance ref",
|
|
0x001a: "GPS dest distance",
|
|
0x001b: "GPS processing method",
|
|
0x001c: "GPS area information",
|
|
0x001d: "GPS datestamp",
|
|
0x001e: "GPS differential",
|
|
|
|
0x0100: "Image width",
|
|
0x0101: "Image height",
|
|
0x0102: "Number of bits per component",
|
|
0x0103: "Compression scheme",
|
|
0x0106: "Pixel composition",
|
|
TAG_ORIENTATION: "Orientation of image",
|
|
0x0115: "Number of components",
|
|
0x011C: "Image data arrangement",
|
|
0x0212: "Subsampling ratio Y to C",
|
|
0x0213: "Y and C positioning",
|
|
0x011A: "Image resolution width direction",
|
|
0x011B: "Image resolution in height direction",
|
|
0x0128: "Unit of X and Y resolution",
|
|
|
|
0x0111: "Image data location",
|
|
0x0116: "Number of rows per strip",
|
|
0x0117: "Bytes per compressed strip",
|
|
0x0201: "Offset to JPEG SOI",
|
|
0x0202: "Bytes of JPEG data",
|
|
|
|
0x012D: "Transfer function",
|
|
0x013E: "White point chromaticity",
|
|
0x013F: "Chromaticities of primaries",
|
|
0x0211: "Color space transformation matrix coefficients",
|
|
0x0214: "Pair of blank and white reference values",
|
|
|
|
TAG_FILE_TIMESTAMP: "File change date and time",
|
|
TAG_IMG_TITLE: "Image title",
|
|
TAG_CAMERA_MANUFACTURER: "Camera (Image input equipment) manufacturer",
|
|
TAG_CAMERA_MODEL: "Camera (Input input equipment) model",
|
|
TAG_SOFTWARE: "Software",
|
|
0x013B: "File change date and time",
|
|
0x8298: "Copyright holder",
|
|
0x8769: "Exif IFD Pointer",
|
|
|
|
TAG_EXPOSURE: "Exposure time",
|
|
TAG_FOCAL: "F number",
|
|
0x8822: "Exposure program",
|
|
0x8824: "Spectral sensitivity",
|
|
0x8827: "ISO speed rating",
|
|
0x8828: "Optoelectric conversion factor OECF",
|
|
0x9201: "Shutter speed",
|
|
0x9202: "Aperture",
|
|
TAG_BRIGHTNESS: "Brightness",
|
|
0x9204: "Exposure bias",
|
|
TAG_APERTURE: "Maximum lens aperture",
|
|
0x9206: "Subject distance",
|
|
0x9207: "Metering mode",
|
|
0x9208: "Light source",
|
|
0x9209: "Flash",
|
|
0x920A: "Lens focal length",
|
|
0x9214: "Subject area",
|
|
0xA20B: "Flash energy",
|
|
0xA20C: "Spatial frequency response",
|
|
0xA20E: "Focal plane X resolution",
|
|
0xA20F: "Focal plane Y resolution",
|
|
0xA210: "Focal plane resolution unit",
|
|
0xA214: "Subject location",
|
|
0xA215: "Exposure index",
|
|
0xA217: "Sensing method",
|
|
0xA300: "File source",
|
|
0xA301: "Scene type",
|
|
0xA302: "CFA pattern",
|
|
0xA401: "Custom image processing",
|
|
0xA402: "Exposure mode",
|
|
0xA403: "White balance",
|
|
0xA404: "Digital zoom ratio",
|
|
0xA405: "Focal length in 35 mm film",
|
|
0xA406: "Scene capture type",
|
|
0xA407: "Gain control",
|
|
0xA408: "Contrast",
|
|
|
|
0x9000: "Exif version",
|
|
0xA000: "Supported Flashpix version",
|
|
0xA001: "Color space information",
|
|
0x9101: "Meaning of each component",
|
|
0x9102: "Image compression mode",
|
|
TAG_WIDTH: "Valid image width",
|
|
TAG_HEIGHT: "Valid image height",
|
|
0x927C: "Manufacturer notes",
|
|
TAG_USER_COMMENT: "User comments",
|
|
0xA004: "Related audio file",
|
|
0x9003: "Date and time of original data generation",
|
|
0x9004: "Date and time of digital data generation",
|
|
0x9290: "DateTime subseconds",
|
|
0x9291: "DateTimeOriginal subseconds",
|
|
0x9292: "DateTimeDigitized subseconds",
|
|
0xA420: "Unique image ID",
|
|
0xA005: "Interoperability IFD Pointer"
|
|
}
|
|
|
|
def createDescription(self):
|
|
return "Entry: %s" % self["tag"].display
|
|
|
|
def sortExifEntry(a,b):
|
|
return int( a["offset"].value - b["offset"].value )
|
|
|
|
class ExifIFD(FieldSet):
|
|
def seek(self, offset):
|
|
"""
|
|
Seek to byte address relative to parent address.
|
|
"""
|
|
padding = offset - (self.address + self.current_size)/8
|
|
if 0 < padding:
|
|
return createPaddingField(self, padding*8)
|
|
else:
|
|
return None
|
|
|
|
def createFields(self):
|
|
offset_diff = 6
|
|
yield UInt16(self, "count", "Number of entries")
|
|
entries = []
|
|
next_chunk_offset = None
|
|
count = self["count"].value
|
|
if not count:
|
|
return
|
|
while count:
|
|
addr = self.absolute_address + self.current_size
|
|
next = self.stream.readBits(addr, 32, NETWORK_ENDIAN)
|
|
if next in (0, 0xF0000000):
|
|
break
|
|
entry = ExifEntry(self, "entry[]")
|
|
yield entry
|
|
if entry["tag"].value in (ExifEntry.EXIF_IFD_POINTER, ExifEntry.OFFSET_JPEG_SOI):
|
|
next_chunk_offset = entry["value"].value + offset_diff
|
|
if 32 < entry.getSizes()[0]:
|
|
entries.append(entry)
|
|
count -= 1
|
|
yield UInt32(self, "next", "Next IFD offset")
|
|
try:
|
|
entries.sort( sortExifEntry )
|
|
except TypeError:
|
|
raise ParserError("Unable to sort entries!")
|
|
value_index = 0
|
|
for entry in entries:
|
|
padding = self.seek(entry["offset"].value + offset_diff)
|
|
if padding is not None:
|
|
yield padding
|
|
|
|
value_size, array_size = entry.getSizes()
|
|
if not array_size:
|
|
continue
|
|
cls = entry.value_cls
|
|
if 1 < array_size:
|
|
name = "value_%s[]" % entry.name
|
|
else:
|
|
name = "value_%s" % entry.name
|
|
desc = "Value of \"%s\"" % entry["tag"].display
|
|
if cls is String:
|
|
for index in xrange(array_size):
|
|
yield cls(self, name, value_size/8, desc, strip=" \0", charset="ISO-8859-1")
|
|
elif cls is Bytes:
|
|
for index in xrange(array_size):
|
|
yield cls(self, name, value_size/8, desc)
|
|
else:
|
|
for index in xrange(array_size):
|
|
yield cls(self, name, desc)
|
|
value_index += 1
|
|
if next_chunk_offset is not None:
|
|
padding = self.seek(next_chunk_offset)
|
|
if padding is not None:
|
|
yield padding
|
|
|
|
def createDescription(self):
|
|
return "Exif IFD (id %s)" % self["id"].value
|
|
|
|
class Exif(FieldSet):
|
|
def createFields(self):
|
|
# Headers
|
|
yield String(self, "header", 6, "Header (Exif\\0\\0)", charset="ASCII")
|
|
if self["header"].value != "Exif\0\0":
|
|
raise ParserError("Invalid EXIF signature!")
|
|
yield String(self, "byte_order", 2, "Byte order", charset="ASCII")
|
|
if self["byte_order"].value not in ("II", "MM"):
|
|
raise ParserError("Invalid endian!")
|
|
if self["byte_order"].value == "II":
|
|
self.endian = LITTLE_ENDIAN
|
|
else:
|
|
self.endian = BIG_ENDIAN
|
|
yield UInt16(self, "version", "TIFF version number")
|
|
yield UInt32(self, "img_dir_ofs", "Next image directory offset")
|
|
while not self.eof:
|
|
addr = self.absolute_address + self.current_size
|
|
tag = self.stream.readBits(addr, 16, NETWORK_ENDIAN)
|
|
if tag == 0xFFD8:
|
|
size = (self._size - self.current_size) // 8
|
|
yield SubFile(self, "thumbnail", size, "Thumbnail (JPEG file)", mime_type="image/jpeg")
|
|
break
|
|
elif tag == 0xFFFF:
|
|
break
|
|
yield ExifIFD(self, "ifd[]", "IFD")
|
|
padding = self.seekBit(self._size)
|
|
if padding is not None:
|
|
yield padding
|
|
|
|
|