diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a216500 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "splimport/apetag"] + path = splimport/apetag + url = https://github.com/robertmuth/apetag.git diff --git a/splimport/apetag b/splimport/apetag new file mode 160000 index 0000000..55f3d7a --- /dev/null +++ b/splimport/apetag @@ -0,0 +1 @@ +Subproject commit 55f3d7a51946787df401573fc92698ed88a2cdee diff --git a/splimport/apev2.ksy b/splimport/apev2.ksy new file mode 100644 index 0000000..2e5b2fe --- /dev/null +++ b/splimport/apev2.ksy @@ -0,0 +1,67 @@ +meta: + id: apev2 + file-extension: apetag + endian: le + +seq: + - id: tag + type: tag + +types: + tag: + seq: + - id: header + type: header + size: 32 + - id: frames + type: frame + repeat: expr + repeat-expr: header.item_count + - id: footer + type: footer + size: 32 + header: + seq: + - id: preamble + contents: 'APETAGEX' + - id: version_number + type: u4 + - id: tag_size + type: u4 + - id: item_count + type: u4 + - id: tag_flags + type: b32 + - id: reserved + contents: [0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0] + size: 8 + + frame: + seq: + - id: item_size + type: u4 + - id: item_flags + type: b32 + - id: item_key + type: str + terminator: 0 + encoding: UTF-8 + - id: item_value + size: item_size + + footer: + seq: + - id: preamble + contents: 'APETAGEX' + - id: version_number + type: u4 + - id: tag_size + type: u4 + - id: item_count + type: u4 + - id: tag_flags + type: b32 + - id: reserved + contents: [0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0] + size: 8 + diff --git a/splimport/apev2.py b/splimport/apev2.py new file mode 100644 index 0000000..45ad903 --- /dev/null +++ b/splimport/apev2.py @@ -0,0 +1,86 @@ +# This is a generated section! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +from pkg_resources import parse_version +from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO + + +if parse_version(ks_version) < parse_version('0.7'): + raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) + +class Apev2(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.tag = self._root.Tag(self._io, self, self._root) + + class Tag(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self._raw_header = self._io.read_bytes(32) + io = KaitaiStream(BytesIO(self._raw_header)) + self.header = self._root.Header(io, self, self._root) + self.frames = [None] * (self.header.item_count) + for i in range(self.header.item_count): + self.frames[i] = self._root.Frame(self._io, self, self._root) + + self._raw_footer = self._io.read_bytes(32) + io = KaitaiStream(BytesIO(self._raw_footer)) + self.footer = self._root.Footer(io, self, self._root) + + + class Header(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.preamble = self._io.ensure_fixed_contents(b"\x41\x50\x45\x54\x41\x47\x45\x58") + self.version_number = self._io.read_u4le() + self.tag_size = self._io.read_u4le() + self.item_count = self._io.read_u4le() + self.tag_flags = self._io.read_bits_int(32) + self._io.align_to_byte() + self.reserved = self._io.ensure_fixed_contents(b"\x00\x00\x00\x00\x00\x00\x00\x00") + + + class Frame(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.item_size = self._io.read_u4le() + self.item_flags = self._io.read_bits_int(32) + self._io.align_to_byte() + self.item_key = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8") + self.item_value = self._io.read_bytes(self.item_size) + + + class Footer(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.preamble = self._io.ensure_fixed_contents(b"\x41\x50\x45\x54\x41\x47\x45\x58") + self.version_number = self._io.read_u4le() + self.tag_size = self._io.read_u4le() + self.item_count = self._io.read_u4le() + self.tag_flags = self._io.read_bits_int(32) + self._io.align_to_byte() + self.reserved = self._io.ensure_fixed_contents(b"\x00\x00\x00\x00\x00\x00\x00\x00") diff --git a/splimport/decode.py b/splimport/decode.py index 5db5e4d..589c258 100644 --- a/splimport/decode.py +++ b/splimport/decode.py @@ -1,10 +1,32 @@ import struct +from apev2 import Apev2 +# apev2 spec: https://wiki.hydrogenaud.io/index.php?title=APEv2_specification + +# parse dates from XLastPlayed and XLastScheduled from SPL. by brainsmoke: +# d=swap32(0x14a63351) +# Y,M,D,h,m,s = 1980+(d>>25), (d>>21)&0xf, (d>>16)&0x1f, (d>>11)&0x1f, (d>>5)&0x3f, (d<<1)&0x3f +# print(Y,M,D,h,m,s) + + +# for swapping endianness of a binary package def swap32(i): return struct.unpack("I", i))[0] -d=swap32(0x14a63351) -Y,M,D,h,m,s = 1980+(d>>25), (d>>21)&0xf, (d>>16)&0x1f, (d>>11)&0x1f, (d>>5)&0x3f, (d<<1)&0x3f +ape = Apev2.from_file('OMYG.apetag') -print(Y,M,D,h,m,s) +for item in ape.tag.frames: + key = item.item_key + value = item.item_value + # print(key, value) + + if key == 'XLastPlayed': + hex = value.hex() + + for i in range(0, len(hex), 8): + hex_chunk = "0x{}".format(hex[i: i+8]) + + d=swap32(int(hex_chunk, 16)) + Y,M,D,h,m,s = 1980+(d>>25), (d>>21)&0xf, (d>>16)&0x1f, (d>>11)&0x1f, (d>>5)&0x3f, (d<<1)&0x3f + print(Y,M,D,h,m,s)