some changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
__pycache__/
|
||||
repear.log
|
||||
3
README
Normal file
3
README
Normal file
@@ -0,0 +1,3 @@
|
||||
This branch includes my edits of the 2009 software. I have removed Shuffle support.
|
||||
|
||||
Target device is an Nano 3G
|
||||
456
iTunesDB.py
456
iTunesDB.py
@@ -19,49 +19,67 @@
|
||||
|
||||
import struct, random, array, sys, os, stat, time
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from functools import cmp_to_key
|
||||
try:
|
||||
from PIL import Image
|
||||
PILAvailable = True
|
||||
except ImportError:
|
||||
PILAvailable = False
|
||||
|
||||
except ImportError: PILAvailable = False
|
||||
|
||||
def DefaultLoggingFunction(text, force_flush=True):
|
||||
sys.stdout.write(text)
|
||||
if force_flush: sys.stdout.flush()
|
||||
log = DefaultLoggingFunction
|
||||
|
||||
################################################################################
|
||||
## some helper classes to represent ITDB records, and some helper functions ##
|
||||
################################################################################
|
||||
|
||||
class Field:
|
||||
def __bytes__(self): raise Exception("abstract function call")
|
||||
def __len__(self): raise Exception("abstract function call")
|
||||
|
||||
class Tags(StrEnum):
|
||||
BD = "mhbd" # Database object, root: everything is a child of this
|
||||
SD = "mhsd" # Data set, stores data of a type
|
||||
LT = "mhlt" # Track list, stores a list of tracks (wow)
|
||||
IT = "mhit" # Track
|
||||
LP = "mhlp" # Stores the playlists
|
||||
YP = "mhyp" # Playlist
|
||||
IP = "mhip" # Playlist item
|
||||
OD = "mhod" # Stores literal data, see http://www.ipodlinux.org/ITunesDB/#Data_Object
|
||||
LA = "mhla" # Album list
|
||||
LF = "mhlf" # File list
|
||||
IF = "mhif" # Image file
|
||||
FD = "mhfd" # Database object of artwork db
|
||||
LI = "mhli" # Image list
|
||||
II = "mhii" # Image object ("Image Item")
|
||||
NI = "mhni" # Image object ("Image Name")
|
||||
|
||||
class F_Tag(Field):
|
||||
def __init__(self, tag: bytes):
|
||||
self.tag = tag
|
||||
assert isinstance(tag, bytes)
|
||||
def __init__(self, tag: Tags):
|
||||
self.tag = tag.value.encode()
|
||||
assert isinstance(self.tag, bytes)
|
||||
def __bytes__(self): return self.tag
|
||||
def __len__(self): return len(self.tag)
|
||||
class F_Bytes(Field):
|
||||
def __init__(self, data: bytes, padding: int = 0):
|
||||
self.data = bytes(data)
|
||||
while len(self) < padding: self.data += b"\0"
|
||||
def __bytes__(self): return self.data
|
||||
def __len__(self): return len(self.data)
|
||||
|
||||
class F_Formatable(Field):
|
||||
def __init__(self, format, value):
|
||||
def __init__(self, format: str | bytes, value: int):
|
||||
self.format = format
|
||||
self.value = int(value)
|
||||
def __bytes__(self): return struct.pack("<"+self.format, self.value)
|
||||
def __bytes__(self): return struct.pack(f"<{self.format}", self.value)
|
||||
def __len__(self): return struct.calcsize(self.format)
|
||||
|
||||
class F_Int64(F_Formatable):
|
||||
def __init__(self, value): F_Formatable.__init__(self, "Q", value)
|
||||
def __init__(self, value: int): F_Formatable.__init__(self, "Q", value)
|
||||
class F_Int32(F_Formatable):
|
||||
def __init__(self, value): F_Formatable.__init__(self, "L", value)
|
||||
def __init__(self, value: int): F_Formatable.__init__(self, "L", value)
|
||||
class F_Int16(F_Formatable):
|
||||
def __init__(self, value): F_Formatable.__init__(self, "H", value)
|
||||
def __init__(self, value: int): F_Formatable.__init__(self, "H", value)
|
||||
class F_Int8(F_Formatable):
|
||||
def __init__(self, value): F_Formatable.__init__(self, "B", value)
|
||||
def __init__(self, value: int): F_Formatable.__init__(self, "B", value)
|
||||
|
||||
class F_HeaderLength(F_Int32):
|
||||
def __init__(self): F_Int32.__init__(self, 0)
|
||||
@@ -82,13 +100,13 @@ class Record:
|
||||
self.child_count_at = None
|
||||
data = b""
|
||||
for field in header:
|
||||
if field.__class__ == F_HeaderLength: self.header_length_at = len(data)
|
||||
if field.__class__ == F_TotalLength: self.total_length_at = len(data)
|
||||
if field.__class__ == F_ChildCount: self.child_count_at = len(data)
|
||||
if isinstance(field, F_HeaderLength): self.header_length_at = len(data)
|
||||
elif isinstance(field, F_TotalLength): self.total_length_at = len(data)
|
||||
elif isinstance(field, F_ChildCount): self.child_count_at = len(data)
|
||||
|
||||
d = field
|
||||
if isinstance(d, str): d = d.encode()
|
||||
elif not isinstance(d, bytes): d = bytes(d)
|
||||
data += d
|
||||
data += bytes(d)
|
||||
if self.header_length_at: data = data[:self.header_length_at] + struct.pack("<L", len(data)) + data[self.header_length_at+4:]
|
||||
self.data = data
|
||||
self.child_count = 0
|
||||
@@ -96,23 +114,18 @@ class Record:
|
||||
self.child_count += count
|
||||
d = obj
|
||||
if isinstance(d, str): d = d.encode()
|
||||
elif not isinstance(d, bytes): d = bytes(d)
|
||||
self.data += d
|
||||
self.data += bytes(d)
|
||||
def __bytes__(self):
|
||||
data = self.data
|
||||
if self.total_length_at: data = data[:self.total_length_at] + struct.pack("<L", len(data)) + data[self.total_length_at+4:]
|
||||
if self.child_count_at: data = data[:self.child_count_at] + struct.pack("<L", self.child_count) + data[self.child_count_at+4:]
|
||||
return data
|
||||
|
||||
def make_compare_key(x):
|
||||
if isinstance(x, str):
|
||||
return x.encode(sys.getfilesystemencoding(), 'replace').lower()
|
||||
elif isinstance(x, bytes):
|
||||
return x.lower()
|
||||
else:
|
||||
return x
|
||||
|
||||
def compare_dict(a, b, fields):
|
||||
def make_compare_key(x):
|
||||
if isinstance(x, str): return x.encode(sys.getfilesystemencoding(), 'replace').lower()
|
||||
elif isinstance(x, bytes): return x.lower()
|
||||
else: return x
|
||||
for field in fields:
|
||||
val_a = make_compare_key(a.get(field, 0))
|
||||
val_b = make_compare_key(b.get(field, 0))
|
||||
@@ -121,10 +134,8 @@ def compare_dict(a, b, fields):
|
||||
val_a = str(val_a) if val_a is not None else ""
|
||||
val_b = str(val_b) if val_b is not None else ""
|
||||
|
||||
if val_a < val_b: # type: ignore
|
||||
return -1
|
||||
elif val_a > val_b: # type: ignore
|
||||
return 1
|
||||
if val_a < val_b: return -1 # type: ignore
|
||||
elif val_a > val_b: return 1 # type: ignore
|
||||
return 0
|
||||
|
||||
def ifelse(condition, then_val, else_val=None):
|
||||
@@ -141,7 +152,6 @@ def mactime2unixtime(t):
|
||||
if not t: return t
|
||||
return t - MAC_TIME_OFFSET + tzoffset
|
||||
|
||||
|
||||
# "fuzzy" mtime comparison, allows for two types of slight deviations:
|
||||
# 1. differences of exact multiples of one hour (usually time zome problems)
|
||||
# 2. differences of less than 2 seconds (FAT timestamps are imprecise)
|
||||
@@ -150,45 +160,38 @@ def compare_mtime(a, b):
|
||||
if diff > 86402: return False
|
||||
return ((diff % 3600) in (0, 1, 2, 3598, 3599))
|
||||
|
||||
|
||||
################################################################################
|
||||
## some higher-level ITDB record classes ##
|
||||
################################################################################
|
||||
|
||||
class StringDataObject(Record):
|
||||
def __init__(self, mhod_type, content):
|
||||
if isinstance(content, bytes): encoded = content
|
||||
else: encoded = content.encode('utf_16_le', 'replace')
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhod"),
|
||||
F_Int32(0x18),
|
||||
super().__init__((
|
||||
F_Tag(Tags.OD),
|
||||
F_Int32(0x18), # Header size
|
||||
F_TotalLength(),
|
||||
F_Int32(mhod_type),
|
||||
F_Padding(8),
|
||||
F_Int32(1),
|
||||
F_Padding(8), # This contains unknown data
|
||||
F_Int32(1), # Position
|
||||
F_Int32(len(encoded)),
|
||||
F_Int32(1),
|
||||
F_Padding(4)
|
||||
F_Int32(1), # Not sure what this is, thought before this was a UTF-8/UTF-16 toggle
|
||||
F_Padding(4) # Unknown
|
||||
))
|
||||
self.add(encoded)
|
||||
|
||||
class OrderDataObject(Record):
|
||||
def __init__(self, order):
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhod"),
|
||||
F_Int32(0x18),
|
||||
F_Int32(0x2C),
|
||||
F_Int32(100),
|
||||
F_Padding(8),
|
||||
F_Int32(order),
|
||||
F_Padding(16)
|
||||
super().__init__((
|
||||
F_Tag(Tags.OD),
|
||||
F_Int32(0x18), # Header size
|
||||
F_Int32(0x2C), # Total lenght, static?
|
||||
F_Int32(100), # Type 100, ipodlinux says "Seems to vary. iTunes uses it for column sizing info as well as an order indicator in playlists."
|
||||
F_Padding(8), # Unknown
|
||||
F_Int32(order), # Position
|
||||
F_Padding(16) # All rest is null
|
||||
))
|
||||
|
||||
|
||||
class TrackItemRecord(Record):
|
||||
def __init__(self, info):
|
||||
if not 'id' in info:
|
||||
raise KeyError("no track ID set")
|
||||
if not 'id' in info: raise KeyError("no track ID set")
|
||||
format = info.get('format', "mp3-cbr")
|
||||
if info.get('artwork', None):
|
||||
default_has_artwork = True
|
||||
@@ -196,34 +199,32 @@ class TrackItemRecord(Record):
|
||||
else:
|
||||
default_has_artwork = False
|
||||
default_artwork_size = 0
|
||||
if 'video format' in info:
|
||||
media_type = 2
|
||||
else:
|
||||
media_type = 1
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhit"),
|
||||
if 'video format' in info: media_type = 2
|
||||
else: media_type = 1
|
||||
super().__init__((
|
||||
F_Tag(Tags.IT), # This describes a track
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_ChildCount(),
|
||||
F_Int32(info.get('id', 0)),
|
||||
F_Int32(info.get('visible', 1)), # visible
|
||||
F_Tag({"mp3": " 3PM", "aac": " CAA", "mp4a": "A4PM"}.get(format[:3], "\0\0\0\0").encode()),
|
||||
F_Int16({"mp3-cbr": 0x100, "mp3-vbr": 0x101, "aac": 0, "mp4a": 0}.get(format, 0)),
|
||||
F_Int8(info.get('compilation', 0)),
|
||||
F_Int8(info.get('rating', 0)),
|
||||
F_Int32(unixtime2mactime(info.get('mtime', 0))),
|
||||
F_Int32(info.get('size', 0)),
|
||||
F_Int32(int(info.get('length', 0) * 1000)),
|
||||
F_Int32(info.get('track number', 0)),
|
||||
F_Int32(info.get('total tracks', 0)),
|
||||
F_Int32(info.get('year', 0)),
|
||||
F_Int32(info.get('bitrate', 0)),
|
||||
F_Int16(0),
|
||||
F_Int16(info.get('sample rate', 0)),
|
||||
F_Int32(info.get('volume', 0)),
|
||||
F_ChildCount(), # Described as number of strings
|
||||
F_Int32(info.get('id', 0)), # ID for the track, for tracking in the playlists
|
||||
F_Int32(info.get('visible', 1)), # visible, hides the track if 0
|
||||
F_Bytes({"mp3": " 3PM", "aac": " CAA", "mp4a": "A4PM"}.get(format[:3], "\0\0\0\0").encode()), # File type, mp3, aac or m4a
|
||||
F_Int16({"mp3-cbr": 0x100, "mp3-vbr": 0x101, "aac": 0, "mp4a": 0}.get(format, 0)), # type1 as in the wiki (and type2?)
|
||||
F_Int8(info.get('compilation', 0)), # is this from a compilation
|
||||
F_Int8(info.get('rating', 0)), # rating times 20, not updated by ipod
|
||||
F_Int32(unixtime2mactime(info.get('mtime', 0))), # mod time
|
||||
F_Int32(info.get('size', 0)), # size of track in bytes
|
||||
F_Int32(int(info.get('length', 0) * 1000)), # length of track in milliseconds
|
||||
F_Int32(info.get('track number', 0)), # track number
|
||||
F_Int32(info.get('total tracks', 0)), # album track count
|
||||
F_Int32(info.get('year', 0)), # track year
|
||||
F_Int32(info.get('bitrate', 0)), # bitrate of track
|
||||
F_Int16(0), # sample rate times 0x10000
|
||||
F_Int16(info.get('sample rate', 0)), # sample rate times 0x10000 (not sure why they split this into two bytes in this impl)
|
||||
F_Int32(info.get('volume', 0)), # -255 to 255 what volume you want at playback
|
||||
F_Int32(info.get('start time', 0)),
|
||||
F_Int32(info.get('stop time', 0)),
|
||||
F_Int32(info.get('soundcheck', 0)),
|
||||
F_Int32(info.get('soundcheck', 0)), # used to normalize tracks
|
||||
F_Int32(info.get('play count', 0)),
|
||||
F_Int32(0),
|
||||
F_Int32(unixtime2mactime(info.get('last played time', 0))),
|
||||
@@ -275,20 +276,19 @@ class TrackItemRecord(Record):
|
||||
for mhod_type, key in ((1,'title'), (4,'artist'), (3,'album'), (5,'genre'), (6,'filetype'), (2,'path')):
|
||||
if key in info:
|
||||
value = info[key]
|
||||
if key=="path":
|
||||
value = ":" + value.replace("/", ":").replace("\\", ":")
|
||||
if key == "path": value = ":" + value.replace("/", ":").replace("\\", ":")
|
||||
self.add(StringDataObject(mhod_type, value))
|
||||
|
||||
|
||||
class PlaylistItemRecord(Record):
|
||||
def __init__(self, order, trackid, timestamp=0):
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhip"),
|
||||
super().__init__((
|
||||
F_Tag(Tags.IP), # Playlist item
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_ChildCount(),
|
||||
F_Int32(0),
|
||||
F_Int32((trackid + 0x1337) & 0xFFFF),
|
||||
F_Int32((trackid + 0x1337) & 0xFFFF), # 1337 is not in the specs...
|
||||
F_Int32(trackid),
|
||||
F_Int32(timestamp),
|
||||
F_Int32(0),
|
||||
@@ -296,12 +296,11 @@ class PlaylistItemRecord(Record):
|
||||
))
|
||||
self.add(OrderDataObject(order))
|
||||
|
||||
|
||||
class PlaylistRecord(Record):
|
||||
def __init__(self, name, track_count, order=0, master=0, timestamp=0, plid=None, sort_order=1):
|
||||
if not plid: plid = random.randrange(0, 18446744073709551615)
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhyp"),
|
||||
super().__init__((
|
||||
F_Tag(Tags.YP),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_ChildCount(),
|
||||
@@ -322,7 +321,7 @@ class PlaylistRecord(Record):
|
||||
order = list(range(len(tracklist)))
|
||||
order.sort(key=cmp_to_key(lambda a, b: compare_dict(tracklist[a], tracklist[b], fields)))
|
||||
mhod = Record((
|
||||
F_Tag(b"mhod"),
|
||||
F_Tag(Tags.OD),
|
||||
F_Int32(24),
|
||||
F_TotalLength(),
|
||||
F_Int32(52),
|
||||
@@ -334,74 +333,63 @@ class PlaylistRecord(Record):
|
||||
arr = array.array('L', order)
|
||||
# the array module doesn't directly support endianness, so we detect
|
||||
# the machine's endianness and swap if it is big-endian
|
||||
if array.array('L', [1]).tobytes()[3] == 1:
|
||||
arr.byteswap()
|
||||
data = bytes(arr)
|
||||
mhod.add(data)
|
||||
if array.array('L', [1]).tobytes()[3] == 1: arr.byteswap()
|
||||
mhod.add(arr)
|
||||
self.add(mhod)
|
||||
|
||||
def set_playlist(self, track_ids):
|
||||
for i in range(len(track_ids)):
|
||||
self.add(PlaylistItemRecord(i+1, track_ids[i]), 0)
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
## the toplevel ITDB class ##
|
||||
################################################################################
|
||||
def set_playlist(self, track_ids): [self.add(PlaylistItemRecord(i+1, track_ids[i]), 0) for i in range(len(track_ids))]
|
||||
|
||||
class iTunesDB:
|
||||
def __init__(self, tracklist, name="Unnamed", dbid=None, dbversion=0x19):
|
||||
if not dbid: dbid = random.randrange(0, 18446744073709551615)
|
||||
def __init__(self, tracklist, name="Unnamed", dbid=None):
|
||||
if not dbid: dbid = random.randrange(0, 0xffffffffffffffff) # random 64 bit integer
|
||||
|
||||
self.mhbd = Record((
|
||||
F_Tag(b"mhbd"),
|
||||
F_Tag(Tags.BD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(0),
|
||||
F_Int32(dbversion),
|
||||
F_Int32(0x19),
|
||||
F_ChildCount(),
|
||||
F_Int64(dbid),
|
||||
F_Int16(2),
|
||||
F_Padding(14),
|
||||
F_Int16(0), # hash indicator (set later by hash58)
|
||||
F_Padding(20), # first hash
|
||||
F_Tag(b"en"), # language = 'en'
|
||||
F_Tag(b"\0rePear!"), # library persistent ID
|
||||
F_Bytes(b"en"), # language = 'en'
|
||||
F_Bytes(b"\0rePear!"), # library persistent ID
|
||||
F_Padding(20), # hash58
|
||||
F_Padding(80)
|
||||
))
|
||||
|
||||
self.mhsd = Record((
|
||||
F_Tag(b"mhsd"),
|
||||
F_Tag(Tags.SD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(1),
|
||||
F_Padding(80)
|
||||
))
|
||||
self.mhlt = Record((
|
||||
F_Tag(b"mhlt"),
|
||||
F_Tag(Tags.LT),
|
||||
F_HeaderLength(),
|
||||
F_ChildCount(),
|
||||
F_Padding(80)
|
||||
))
|
||||
|
||||
for track in tracklist:
|
||||
self.mhlt.add(TrackItemRecord(track))
|
||||
for track in tracklist: self.mhlt.add(TrackItemRecord(track))
|
||||
|
||||
self.mhsd.add(self.mhlt)
|
||||
del self.mhlt
|
||||
self.mhbd.add(self.mhsd)
|
||||
|
||||
self.mhsd = Record((
|
||||
F_Tag(b"mhsd"),
|
||||
F_Tag(Tags.SD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(2),
|
||||
F_Padding(80)
|
||||
))
|
||||
self.mhlp = Record((
|
||||
F_Tag(b"mhlp"),
|
||||
F_Tag(Tags.LP),
|
||||
F_HeaderLength(),
|
||||
F_ChildCount(),
|
||||
F_Padding(80)
|
||||
@@ -426,12 +414,10 @@ class iTunesDB:
|
||||
del self.mhlp
|
||||
self.mhbd.add(self.mhsd)
|
||||
del self.mhsd
|
||||
result = self.mhbd.__bytes__()
|
||||
result = bytes(self.mhbd)
|
||||
del self.mhbd
|
||||
return result
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
## ArtworkDB / PhotoDB record classes ##
|
||||
################################################################################
|
||||
@@ -448,7 +434,6 @@ class RGB565_LE:
|
||||
res[io|1] = (ord(data[ii]) & 0xF8) | (g >> 3)
|
||||
io += 2
|
||||
return str(res)
|
||||
convert = staticmethod(convert)
|
||||
|
||||
ImageFormats = {
|
||||
'nano': ((1027, 100, 100, RGB565_LE),
|
||||
@@ -491,38 +476,27 @@ class ArtworkFormat:
|
||||
# check if the cache file can be used
|
||||
try:
|
||||
s = os.stat(self.fullname)
|
||||
use_cache = stat.S_ISREG(s[stat.ST_MODE]) \
|
||||
and compare_mtime(cache_info[0], s[stat.ST_MTIME]) \
|
||||
and (s[stat.ST_SIZE] == cache_info[1])
|
||||
except OSError:
|
||||
use_cache = False
|
||||
use_cache = stat.S_ISREG(s[stat.ST_MODE]) and compare_mtime(cache_info[0], s[stat.ST_MTIME]) and (s[stat.ST_SIZE] == cache_info[1])
|
||||
except OSError: use_cache = False
|
||||
|
||||
# load the cache
|
||||
if use_cache:
|
||||
try:
|
||||
f = open(self.fullname, "rb")
|
||||
self.cache = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
use_cache = False
|
||||
if not use_cache:
|
||||
self.cache = None
|
||||
with open(self.fullname, "rb") as f: self.cache = f.read()
|
||||
except IOError: use_cache = False
|
||||
if not use_cache: self.cache = None
|
||||
|
||||
# open the destination file
|
||||
try:
|
||||
self.f = open(self.fullname, "wb")
|
||||
except IOError as e:
|
||||
try: self.f = open(self.fullname, "wb")
|
||||
except IOError:
|
||||
log("WARNING: Error opening the artwork data file `%s'\n", self.filename)
|
||||
self.f = None
|
||||
|
||||
def close(self):
|
||||
if self.f:
|
||||
self.f.close()
|
||||
if self.f: self.f.close()
|
||||
try:
|
||||
s = os.stat(self.fullname)
|
||||
cache_info = (s[stat.ST_MTIME], s[stat.ST_SIZE])
|
||||
except OSError:
|
||||
cache_info = (0, 0)
|
||||
except OSError: cache_info = (0, 0)
|
||||
return (self.fid, cache_info)
|
||||
|
||||
def GenerateImage(self, image, index, cache_entry=None):
|
||||
@@ -559,10 +533,8 @@ class ArtworkFormat:
|
||||
assert self.f
|
||||
self.f.seek(self.size * index)
|
||||
self.f.write(data)
|
||||
except IOError:
|
||||
log(" [WRITE ERROR]", True)
|
||||
except IOError: log(" [WRITE ERROR]", True)
|
||||
|
||||
# return image metadata
|
||||
iinfo = ImageInfo()
|
||||
iinfo.format = self
|
||||
iinfo.index = index
|
||||
@@ -572,19 +544,15 @@ class ArtworkFormat:
|
||||
iinfo.my = my
|
||||
return iinfo
|
||||
|
||||
|
||||
|
||||
class ArtworkDBStringDataObject(Record):
|
||||
def __init__(self, mhod_type, content):
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode(sys.getfilesystemencoding(), 'replace')
|
||||
elif not isinstance(content, str):
|
||||
content = str(content)
|
||||
if isinstance(content, bytes): content = content.decode(sys.getfilesystemencoding(), 'replace')
|
||||
elif not isinstance(content, str): content = str(content)
|
||||
content = content.encode('utf_16_le', 'replace')
|
||||
padding = len(content) % 4
|
||||
if padding: padding = 4 - padding
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhod"),
|
||||
super().__init__((
|
||||
F_Tag(Tags.OD),
|
||||
F_Int32(0x18),
|
||||
F_TotalLength(),
|
||||
F_Int16(mhod_type),
|
||||
@@ -595,14 +563,13 @@ class ArtworkDBStringDataObject(Record):
|
||||
F_Int32(0)
|
||||
))
|
||||
self.add(content)
|
||||
if padding:
|
||||
self.add("\0" * padding)
|
||||
if padding: self.add("\0" * padding)
|
||||
|
||||
|
||||
class ImageDataObject(Record):
|
||||
def __init__(self, iinfo):
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhod"),
|
||||
super().__init__( (
|
||||
F_Tag(Tags.OD),
|
||||
F_Int32(0x18),
|
||||
F_TotalLength(),
|
||||
F_Int32(2),
|
||||
@@ -610,7 +577,7 @@ class ImageDataObject(Record):
|
||||
))
|
||||
|
||||
mhni = Record((
|
||||
F_Tag(b"mhni"),
|
||||
F_Tag(Tags.NI),
|
||||
F_Int32(0x4C),
|
||||
F_TotalLength(),
|
||||
F_ChildCount(),
|
||||
@@ -626,15 +593,14 @@ class ImageDataObject(Record):
|
||||
F_Padding(32)
|
||||
))
|
||||
|
||||
mhod = ArtworkDBStringDataObject(3, ":" + iinfo.format.filename)
|
||||
mhni.add(mhod)
|
||||
mhni.add(ArtworkDBStringDataObject(3, ":" + iinfo.format.filename))
|
||||
self.add(mhni)
|
||||
|
||||
|
||||
class ImageItemRecord(Record):
|
||||
def __init__(self, img_id, dbid, iinfo_list, orig_size=0):
|
||||
Record.__init__(self, (
|
||||
F_Tag(b"mhii"),
|
||||
super().__init__( (
|
||||
F_Tag(Tags.II),
|
||||
F_Int32(0x98),
|
||||
F_TotalLength(),
|
||||
F_ChildCount(),
|
||||
@@ -644,37 +610,31 @@ class ImageItemRecord(Record):
|
||||
F_Int32(orig_size),
|
||||
F_Padding(100)
|
||||
))
|
||||
|
||||
for iinfo in iinfo_list:
|
||||
self.add(ImageDataObject(iinfo))
|
||||
for iinfo in iinfo_list: self.add(ImageDataObject(iinfo))
|
||||
|
||||
|
||||
def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
while isinstance(ImageFormats.get(model, None), str):
|
||||
model = ImageFormats[model]
|
||||
if not model in ImageFormats:
|
||||
return None
|
||||
while isinstance(ImageFormats.get(model, None), str): model = ImageFormats[model]
|
||||
if not model in ImageFormats: return None
|
||||
|
||||
format_cache, image_cache = cache_data
|
||||
formats = []
|
||||
for descriptor in ImageFormats[model]:
|
||||
formats.append(ArtworkFormat(descriptor,
|
||||
cache_info = format_cache.get(descriptor[0], (0,0))))
|
||||
formats.append(ArtworkFormat(descriptor, cache_info = format_cache.get(descriptor[0], (0,0))))
|
||||
# if there's at least one format whose image file isn't cache-clean,
|
||||
# invalidate the cache
|
||||
if not formats[-1].cache:
|
||||
image_cache = {}
|
||||
if not formats[-1].cache: image_cache = {}
|
||||
|
||||
# Image List
|
||||
mhsd = Record((
|
||||
F_Tag(b"mhsd"),
|
||||
F_Tag(Tags.SD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(1),
|
||||
F_Padding(80)
|
||||
))
|
||||
mhli = Record((
|
||||
F_Tag(b"mhli"),
|
||||
F_Tag(Tags.LI),
|
||||
F_HeaderLength(),
|
||||
F_ChildCount(),
|
||||
F_Padding(80)
|
||||
@@ -689,8 +649,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
log(source, False)
|
||||
|
||||
# stat this image
|
||||
try:
|
||||
s = os.stat(source)
|
||||
try: s = os.stat(source)
|
||||
except OSError as e:
|
||||
log(" [Error: %s]\n" % e.strerror, True)
|
||||
continue
|
||||
@@ -746,7 +705,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
|
||||
# Date File Header
|
||||
mhfd = Record((
|
||||
F_Tag(b"mhfd"),
|
||||
F_Tag(Tags.FD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(0),
|
||||
@@ -764,14 +723,14 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
|
||||
# Album List (dummy)
|
||||
mhsd = Record((
|
||||
F_Tag(b"mhsd"),
|
||||
F_Tag(Tags.SD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(2),
|
||||
F_Padding(80)
|
||||
))
|
||||
mhsd.add(Record((
|
||||
F_Tag(b"mhla"),
|
||||
F_Tag(Tags.LA),
|
||||
F_HeaderLength(),
|
||||
F_Int32(0),
|
||||
F_Padding(80)
|
||||
@@ -780,7 +739,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
|
||||
# File List
|
||||
mhsd = Record((
|
||||
F_Tag(b"mhsd"),
|
||||
F_Tag(Tags.SD),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(3),
|
||||
@@ -788,7 +747,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
))
|
||||
|
||||
mhlf = Record((
|
||||
F_Tag(b"mhlf"),
|
||||
F_Tag(Tags.LF),
|
||||
F_HeaderLength(),
|
||||
F_Int32(len(formats)),
|
||||
F_Padding(80)
|
||||
@@ -796,7 +755,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
|
||||
|
||||
for format in formats:
|
||||
mhlf.add(Record((
|
||||
F_Tag(b"mhif"),
|
||||
F_Tag(Tags.IF),
|
||||
F_HeaderLength(),
|
||||
F_TotalLength(),
|
||||
F_Int32(0),
|
||||
@@ -834,91 +793,67 @@ class InvalidFormat(Exception): pass
|
||||
|
||||
class DatabaseReader:
|
||||
def __init__(self, f="iPod_Control/iTunes/iTunesDB"):
|
||||
if isinstance(f, str):
|
||||
f = open(f, "rb")
|
||||
if isinstance(f, str): f = open(f, "rb")
|
||||
self.f = f
|
||||
self._skip_header("mhbd")
|
||||
self._skip_header(Tags.BD)
|
||||
while True:
|
||||
h = self._skip_header("mhsd")
|
||||
if len(h) < 16:
|
||||
raise InvalidFormat
|
||||
h = self._skip_header(Tags.SD)
|
||||
if len(h) < 16: raise InvalidFormat
|
||||
size, mhsd_type = struct.unpack('<LL', h[8:16])
|
||||
if mhsd_type == 1:
|
||||
break # found the mhlt entry -> yeah!
|
||||
if size < len(h):
|
||||
raise InvalidFormat
|
||||
if mhsd_type == 1: break # found the mhlt entry -> yeah!
|
||||
if size < len(h): raise InvalidFormat
|
||||
self.f.seek(size - len(h), 1)
|
||||
self._skip_header("mhlt")
|
||||
self._skip_header(Tags.LT)
|
||||
|
||||
def _skip_header(self, tag): # a little helper function
|
||||
def _skip_header(self, tag: Tags):
|
||||
hh = self.f.read(8)
|
||||
if (len(hh) != 8) or (hh[:4] != tag):
|
||||
raise InvalidFormat
|
||||
if (len(hh) != 8) or (hh[:4] != tag.value.encode()): raise InvalidFormat
|
||||
size = struct.unpack('<L', hh[4:])[0]
|
||||
if size < 8:
|
||||
raise InvalidFormat
|
||||
if size < 8: raise InvalidFormat
|
||||
return hh + self.f.read(size - 8)
|
||||
|
||||
def __iter__(self): return self
|
||||
def next(self):
|
||||
try:
|
||||
header = self._skip_header("mhit")
|
||||
except (IOError, InvalidFormat):
|
||||
raise StopIteration
|
||||
try: header = self._skip_header(Tags.IT)
|
||||
except (IOError, InvalidFormat): raise StopIteration
|
||||
data_size = struct.unpack('<L', header[8:12])[0] - len(header)
|
||||
if data_size<0:
|
||||
raise InvalidFormat
|
||||
if data_size<0: raise InvalidFormat
|
||||
|
||||
info = {}
|
||||
data = self.f.read(data_size)
|
||||
if len(data) < 48:
|
||||
raise InvalidFormat
|
||||
if len(data) < 48: raise InvalidFormat
|
||||
trk = struct.unpack('<L', header[44:48])[0]
|
||||
if trk: info['track number'] = trk
|
||||
|
||||
# walk through mhods
|
||||
while (len(data) > 40) and (data[:4] == "mhod"):
|
||||
while (len(data) > 40) and (data[:4] == b"mhod"):
|
||||
size, mhod_type = struct.unpack('<LL', data[8:16])
|
||||
value = str(data[40:size], "utf_16_le", 'replace')
|
||||
if mhod_type in mhod_type_map:
|
||||
info[mhod_type_map[mhod_type]] = value
|
||||
if mhod_type in mhod_type_map: info[mhod_type_map[mhod_type]] = value
|
||||
data = data[size:]
|
||||
return info
|
||||
|
||||
|
||||
################################################################################
|
||||
## Play Counts file reader ##
|
||||
################################################################################
|
||||
|
||||
class PlayCountsItem:
|
||||
def __init__(self, data, index):
|
||||
self.index = index
|
||||
self.play_count, \
|
||||
t_last_played, \
|
||||
self.bookmark, \
|
||||
self.rating, \
|
||||
dummy, \
|
||||
self.skip_count, \
|
||||
t_last_skipped = \
|
||||
struct.unpack("<LLLLLLL", data + "\0" * (28 - len(data)))
|
||||
self.play_count, t_last_played, self.bookmark, \
|
||||
self.rating, dummy, self.skip_count, \
|
||||
t_last_skipped = struct.unpack("<LLLLLLL", data + b"\0" * (28 - len(data)))
|
||||
|
||||
self.last_played = mactime2unixtime(t_last_played)
|
||||
self.last_skipped = mactime2unixtime(t_last_skipped)
|
||||
|
||||
class PlayCountsReader:
|
||||
def __init__(self, f="iPod_Control/iTunes/Play Counts"):
|
||||
if isinstance(f, str):
|
||||
f = open(f, "rb")
|
||||
if isinstance(f, str): f = open(f, "rb")
|
||||
self.f = f
|
||||
self.f.seek(0, 2)
|
||||
self.file_size = self.f.tell()
|
||||
self.f.seek(0)
|
||||
if self.file_size < 16:
|
||||
raise InvalidFormat
|
||||
if self.f.read(4) != "mhdp":
|
||||
raise InvalidFormat
|
||||
if self.file_size < 16: raise InvalidFormat
|
||||
if self.f.read(4) != b"mhdp": raise InvalidFormat
|
||||
header_size, self.entry_size, self.entry_count = struct.unpack("<LLL", f.read(12))
|
||||
if self.file_size != (header_size + self.entry_size * self.entry_count):
|
||||
raise InvalidFormat
|
||||
if self.file_size != (header_size + self.entry_size * self.entry_count): raise InvalidFormat
|
||||
self.f.seek(header_size)
|
||||
self.index = 0
|
||||
|
||||
@@ -929,50 +864,15 @@ class PlayCountsReader:
|
||||
self.index += 1
|
||||
return PlayCountsItem(data[:28], self.index-1)
|
||||
|
||||
|
||||
################################################################################
|
||||
## an iTunesSD generator (for iPod shuffle devices) ##
|
||||
################################################################################
|
||||
|
||||
def be3(x):
|
||||
return b"%c%c%c" % (x >> 16, (x >> 8) & 0xFF, x & 0xFF)
|
||||
|
||||
SD_type_map = { "aac": 2, "mp4a": 2, "wave": 4}
|
||||
|
||||
def MakeSDEntry(info):
|
||||
path = info['path']
|
||||
|
||||
if isinstance(path, bytes):
|
||||
path = path.decode(sys.getfilesystemencoding(), 'replace')
|
||||
elif not isinstance(path, str):
|
||||
path = str(path)
|
||||
|
||||
path_bytes = ('/' + path).encode("utf_16_le", 'replace')
|
||||
|
||||
return b"\0\x02\x2E\x5A\xA5\x01" + (20 * b"\0") + b"\x64\0\0" + bytes([SD_type_map.get(info.get('type', None), 1)]) + b"\0\x02\0" + \
|
||||
path_bytes + (261 * 2 - len(path_bytes)) * b"\0" + bytes([info.get('shuffle flag', 1), info.get('bookmark flag', 0), 0])
|
||||
|
||||
|
||||
def iTunesSD(tracklist):
|
||||
header = b"\0\x02\x2E\x5A\xA5\x01" + (20*b"\0") + b"\x64\0\0\0x01\0\0x02\0"
|
||||
return be3(len(tracklist)) + b"\x01\x06\0\0\0\x12" + (9*b"\0") + \
|
||||
b"".join(map(MakeSDEntry, tracklist))
|
||||
|
||||
|
||||
################################################################################
|
||||
## some useful helper functions for "fine tuning" of track lists ##
|
||||
################################################################################
|
||||
|
||||
def GenerateIDs(tracklist):
|
||||
trackid = random.randint(0, (0xFFFF-0x1337) - len(tracklist))
|
||||
dbid = random.randrange(0, 18446744073709551615 - len(tracklist))
|
||||
dbid = random.randrange(0, 0xffffffffffffffff - len(tracklist))
|
||||
for track in tracklist:
|
||||
track['id'] = trackid
|
||||
track['dbid'] = dbid
|
||||
trackid += 1
|
||||
dbid += 1
|
||||
|
||||
|
||||
def GuessTitleAndArtist(filename):
|
||||
info = {}
|
||||
filename = os.path.split(filename)[1]
|
||||
@@ -992,14 +892,12 @@ def GuessTitleAndArtist(filename):
|
||||
if len(parts) == 2:
|
||||
info['artist'] = parts[0].strip()
|
||||
info['title'] = parts[1].strip(" -\r\n\t\v")
|
||||
else:
|
||||
info['title'] = filename.strip()
|
||||
else: info['title'] = filename.strip()
|
||||
return info
|
||||
|
||||
def FillMissingTitleAndArtist(track_or_list):
|
||||
if isinstance(track_or_list, list):
|
||||
for track in track_or_list:
|
||||
FillMissingTitleAndArtist(track)
|
||||
for track in track_or_list: FillMissingTitleAndArtist(track)
|
||||
else:
|
||||
if track_or_list.get('title',None) and track_or_list.get('artist',None):
|
||||
return # no need to do something, it's fine already
|
||||
@@ -1007,21 +905,3 @@ def FillMissingTitleAndArtist(track_or_list):
|
||||
for key in ('title', 'artist', 'track number'):
|
||||
if not(track_or_list.get(key,None)) and guess.get(key,None):
|
||||
track_or_list[key] = guess[key]
|
||||
|
||||
|
||||
################################################################################
|
||||
## some additional general purpose helper functions ##
|
||||
################################################################################
|
||||
|
||||
def ASCIIMap(c):
|
||||
if ord(c) < 32: return "."
|
||||
if ord(c) == 127: return "."
|
||||
return c
|
||||
|
||||
def DisplayTitle(info):
|
||||
s = info.get('title', "")
|
||||
if 'album' in info: s = "%s -> %s" % ((info['album']), s)
|
||||
if 'artist' in info: s = "%s: %s" % ((info['artist']), s)
|
||||
q = [str((info[key])) for key in ('genre','year') if key in info]
|
||||
if q: s = "%s [%s]" % (s, ", ".join(q))
|
||||
return s
|
||||
10
repear.log
10
repear.log
@@ -1,10 +0,0 @@
|
||||
Welcome to rePear, version 0.4.1
|
||||
--------------------------------
|
||||
|
||||
iPod root directory is `F:/'
|
||||
|
||||
Moving tracks back to their original locations ...
|
||||
s2/Sade - Your Love Is King.mp3 [OK]
|
||||
Operation complete: 1 tracks total, 1 moved back, 0 failed.
|
||||
|
||||
You can now manage the music files on your iPod.
|
||||
22
repear.py
22
repear.py
@@ -42,10 +42,6 @@ from pathlib import Path
|
||||
import iTunesDB, mp3info, hash58
|
||||
Options = {}
|
||||
|
||||
################################################################################
|
||||
## Some internal management functions ##
|
||||
################################################################################
|
||||
|
||||
broken_log = False
|
||||
homedir = ""
|
||||
logfile = None
|
||||
@@ -1369,24 +1365,6 @@ NOTE: The database is already frozen.
|
||||
"ERROR: The iTunesDB file could not be written. This means that the iPod will\n" +
|
||||
"not play anything.\n")
|
||||
|
||||
# write iPod shuffle stuff (if necessary)
|
||||
if os.path.exists(CONTROL_DIR + "iTunesSD"):
|
||||
backup(CONTROL_DIR + "iTunesSD")
|
||||
log("Creating iTunesSD ... ", True)
|
||||
db = iTunesDB.iTunesSD(tracklist)
|
||||
try:
|
||||
f = open(CONTROL_DIR + "iTunesSD", "wb")
|
||||
f.write(db)
|
||||
f.close()
|
||||
log("\n")
|
||||
except IOError as e:
|
||||
write_ok = False
|
||||
log("FAILED: %s\n" % e.strerror +
|
||||
"ERROR: The iTunesSD file could not be written. This means that the iPod will\n" +
|
||||
"not play anything.\n")
|
||||
delete(CONTROL_DIR + "iTunesShuffle")
|
||||
delete(CONTROL_DIR + "iTunesPState")
|
||||
|
||||
# generate statistics
|
||||
if write_ok:
|
||||
log("\nYou can now unmount the iPod and listen to your music.\n")
|
||||
|
||||
Reference in New Issue
Block a user