some changes

This commit is contained in:
2025-12-01 15:10:55 +01:00
parent 80aeedfd38
commit ee0154fa89
5 changed files with 178 additions and 326 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
__pycache__/ __pycache__/
repear.log

3
README Normal file
View 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

View File

@@ -19,49 +19,67 @@
import struct, random, array, sys, os, stat, time import struct, random, array, sys, os, stat, time
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum
from functools import cmp_to_key from functools import cmp_to_key
try: try:
from PIL import Image from PIL import Image
PILAvailable = True PILAvailable = True
except ImportError: except ImportError: PILAvailable = False
PILAvailable = False
def DefaultLoggingFunction(text, force_flush=True): def DefaultLoggingFunction(text, force_flush=True):
sys.stdout.write(text) sys.stdout.write(text)
if force_flush: sys.stdout.flush() if force_flush: sys.stdout.flush()
log = DefaultLoggingFunction log = DefaultLoggingFunction
################################################################################
## some helper classes to represent ITDB records, and some helper functions ##
################################################################################
class Field: class Field:
def __bytes__(self): raise Exception("abstract function call") def __bytes__(self): raise Exception("abstract function call")
def __len__(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): class F_Tag(Field):
def __init__(self, tag: bytes): def __init__(self, tag: Tags):
self.tag = tag self.tag = tag.value.encode()
assert isinstance(tag, bytes) assert isinstance(self.tag, bytes)
def __bytes__(self): return self.tag def __bytes__(self): return self.tag
def __len__(self): return len(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): class F_Formatable(Field):
def __init__(self, format, value): def __init__(self, format: str | bytes, value: int):
self.format = format self.format = format
self.value = int(value) 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) def __len__(self): return struct.calcsize(self.format)
class F_Int64(F_Formatable): 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): 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): 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): 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): class F_HeaderLength(F_Int32):
def __init__(self): F_Int32.__init__(self, 0) def __init__(self): F_Int32.__init__(self, 0)
@@ -82,13 +100,13 @@ class Record:
self.child_count_at = None self.child_count_at = None
data = b"" data = b""
for field in header: for field in header:
if field.__class__ == F_HeaderLength: self.header_length_at = len(data) if isinstance(field, F_HeaderLength): self.header_length_at = len(data)
if field.__class__ == F_TotalLength: self.total_length_at = len(data) elif isinstance(field, F_TotalLength): self.total_length_at = len(data)
if field.__class__ == F_ChildCount: self.child_count_at = len(data) elif isinstance(field, F_ChildCount): self.child_count_at = len(data)
d = field d = field
if isinstance(d, str): d = d.encode() if isinstance(d, str): d = d.encode()
elif not isinstance(d, bytes): d = bytes(d) data += bytes(d)
data += d
if self.header_length_at: data = data[:self.header_length_at] + struct.pack("<L", len(data)) + data[self.header_length_at+4:] 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.data = data
self.child_count = 0 self.child_count = 0
@@ -96,35 +114,28 @@ class Record:
self.child_count += count self.child_count += count
d = obj d = obj
if isinstance(d, str): d = d.encode() if isinstance(d, str): d = d.encode()
elif not isinstance(d, bytes): d = bytes(d) self.data += bytes(d)
self.data += d
def __bytes__(self): def __bytes__(self):
data = self.data 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.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:] 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 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 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: for field in fields:
val_a = make_compare_key(a.get(field, 0)) val_a = make_compare_key(a.get(field, 0))
val_b = make_compare_key(b.get(field, 0)) val_b = make_compare_key(b.get(field, 0))
if type(val_a) != type(val_b): if type(val_a) != type(val_b):
val_a = str(val_a) if val_a is not None else "" val_a = str(val_a) if val_a is not None else ""
val_b = str(val_b) if val_b is not None else "" val_b = str(val_b) if val_b is not None else ""
if val_a < val_b: # type: ignore if val_a < val_b: return -1 # type: ignore
return -1 elif val_a > val_b: return 1 # type: ignore
elif val_a > val_b: # type: ignore
return 1
return 0 return 0
def ifelse(condition, then_val, else_val=None): def ifelse(condition, then_val, else_val=None):
@@ -133,7 +144,7 @@ def ifelse(condition, then_val, else_val=None):
MAC_TIME_OFFSET = 2082844800 MAC_TIME_OFFSET = 2082844800
if time.daylight: tzoffset = time.altzone if time.daylight: tzoffset = time.altzone
else: tzoffset = time.timezone else: tzoffset = time.timezone
def unixtime2mactime(t): def unixtime2mactime(t):
if not t: return t if not t: return t
return t + MAC_TIME_OFFSET - tzoffset return t + MAC_TIME_OFFSET - tzoffset
@@ -141,7 +152,6 @@ def mactime2unixtime(t):
if not t: return t if not t: return t
return t - MAC_TIME_OFFSET + tzoffset return t - MAC_TIME_OFFSET + tzoffset
# "fuzzy" mtime comparison, allows for two types of slight deviations: # "fuzzy" mtime comparison, allows for two types of slight deviations:
# 1. differences of exact multiples of one hour (usually time zome problems) # 1. differences of exact multiples of one hour (usually time zome problems)
# 2. differences of less than 2 seconds (FAT timestamps are imprecise) # 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 if diff > 86402: return False
return ((diff % 3600) in (0, 1, 2, 3598, 3599)) return ((diff % 3600) in (0, 1, 2, 3598, 3599))
################################################################################
## some higher-level ITDB record classes ##
################################################################################
class StringDataObject(Record): class StringDataObject(Record):
def __init__(self, mhod_type, content): def __init__(self, mhod_type, content):
if isinstance(content, bytes): encoded = content if isinstance(content, bytes): encoded = content
else: encoded = content.encode('utf_16_le', 'replace') else: encoded = content.encode('utf_16_le', 'replace')
Record.__init__(self, ( super().__init__((
F_Tag(b"mhod"), F_Tag(Tags.OD),
F_Int32(0x18), F_Int32(0x18), # Header size
F_TotalLength(), F_TotalLength(),
F_Int32(mhod_type), F_Int32(mhod_type),
F_Padding(8), F_Padding(8), # This contains unknown data
F_Int32(1), F_Int32(1), # Position
F_Int32(len(encoded)), F_Int32(len(encoded)),
F_Int32(1), F_Int32(1), # Not sure what this is, thought before this was a UTF-8/UTF-16 toggle
F_Padding(4) F_Padding(4) # Unknown
)) ))
self.add(encoded) self.add(encoded)
class OrderDataObject(Record): class OrderDataObject(Record):
def __init__(self, order): def __init__(self, order):
Record.__init__(self, ( super().__init__((
F_Tag(b"mhod"), F_Tag(Tags.OD),
F_Int32(0x18), F_Int32(0x18), # Header size
F_Int32(0x2C), F_Int32(0x2C), # Total lenght, static?
F_Int32(100), 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), F_Padding(8), # Unknown
F_Int32(order), F_Int32(order), # Position
F_Padding(16) F_Padding(16) # All rest is null
)) ))
class TrackItemRecord(Record): class TrackItemRecord(Record):
def __init__(self, info): def __init__(self, info):
if not 'id' in info: if not 'id' in info: raise KeyError("no track ID set")
raise KeyError("no track ID set")
format = info.get('format', "mp3-cbr") format = info.get('format', "mp3-cbr")
if info.get('artwork', None): if info.get('artwork', None):
default_has_artwork = True default_has_artwork = True
@@ -196,34 +199,32 @@ class TrackItemRecord(Record):
else: else:
default_has_artwork = False default_has_artwork = False
default_artwork_size = 0 default_artwork_size = 0
if 'video format' in info: if 'video format' in info: media_type = 2
media_type = 2 else: media_type = 1
else: super().__init__((
media_type = 1 F_Tag(Tags.IT), # This describes a track
Record.__init__(self, (
F_Tag(b"mhit"),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_ChildCount(), F_ChildCount(), # Described as number of strings
F_Int32(info.get('id', 0)), F_Int32(info.get('id', 0)), # ID for the track, for tracking in the playlists
F_Int32(info.get('visible', 1)), # visible F_Int32(info.get('visible', 1)), # visible, hides the track if 0
F_Tag({"mp3": " 3PM", "aac": " CAA", "mp4a": "A4PM"}.get(format[:3], "\0\0\0\0").encode()), 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)), 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)), F_Int8(info.get('compilation', 0)), # is this from a compilation
F_Int8(info.get('rating', 0)), F_Int8(info.get('rating', 0)), # rating times 20, not updated by ipod
F_Int32(unixtime2mactime(info.get('mtime', 0))), F_Int32(unixtime2mactime(info.get('mtime', 0))), # mod time
F_Int32(info.get('size', 0)), F_Int32(info.get('size', 0)), # size of track in bytes
F_Int32(int(info.get('length', 0) * 1000)), F_Int32(int(info.get('length', 0) * 1000)), # length of track in milliseconds
F_Int32(info.get('track number', 0)), F_Int32(info.get('track number', 0)), # track number
F_Int32(info.get('total tracks', 0)), F_Int32(info.get('total tracks', 0)), # album track count
F_Int32(info.get('year', 0)), F_Int32(info.get('year', 0)), # track year
F_Int32(info.get('bitrate', 0)), F_Int32(info.get('bitrate', 0)), # bitrate of track
F_Int16(0), F_Int16(0), # sample rate times 0x10000
F_Int16(info.get('sample rate', 0)), 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)), 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('start time', 0)),
F_Int32(info.get('stop 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(info.get('play count', 0)),
F_Int32(0), F_Int32(0),
F_Int32(unixtime2mactime(info.get('last played time', 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')): for mhod_type, key in ((1,'title'), (4,'artist'), (3,'album'), (5,'genre'), (6,'filetype'), (2,'path')):
if key in info: if key in info:
value = info[key] value = info[key]
if key=="path": if key == "path": value = ":" + value.replace("/", ":").replace("\\", ":")
value = ":" + value.replace("/", ":").replace("\\", ":")
self.add(StringDataObject(mhod_type, value)) self.add(StringDataObject(mhod_type, value))
class PlaylistItemRecord(Record): class PlaylistItemRecord(Record):
def __init__(self, order, trackid, timestamp=0): def __init__(self, order, trackid, timestamp=0):
Record.__init__(self, ( super().__init__((
F_Tag(b"mhip"), F_Tag(Tags.IP), # Playlist item
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_ChildCount(), F_ChildCount(),
F_Int32(0), F_Int32(0),
F_Int32((trackid + 0x1337) & 0xFFFF), F_Int32((trackid + 0x1337) & 0xFFFF), # 1337 is not in the specs...
F_Int32(trackid), F_Int32(trackid),
F_Int32(timestamp), F_Int32(timestamp),
F_Int32(0), F_Int32(0),
@@ -296,12 +296,11 @@ class PlaylistItemRecord(Record):
)) ))
self.add(OrderDataObject(order)) self.add(OrderDataObject(order))
class PlaylistRecord(Record): class PlaylistRecord(Record):
def __init__(self, name, track_count, order=0, master=0, timestamp=0, plid=None, sort_order=1): 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) if not plid: plid = random.randrange(0, 18446744073709551615)
Record.__init__(self, ( super().__init__((
F_Tag(b"mhyp"), F_Tag(Tags.YP),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_ChildCount(), F_ChildCount(),
@@ -322,7 +321,7 @@ class PlaylistRecord(Record):
order = list(range(len(tracklist))) order = list(range(len(tracklist)))
order.sort(key=cmp_to_key(lambda a, b: compare_dict(tracklist[a], tracklist[b], fields))) order.sort(key=cmp_to_key(lambda a, b: compare_dict(tracklist[a], tracklist[b], fields)))
mhod = Record(( mhod = Record((
F_Tag(b"mhod"), F_Tag(Tags.OD),
F_Int32(24), F_Int32(24),
F_TotalLength(), F_TotalLength(),
F_Int32(52), F_Int32(52),
@@ -334,74 +333,63 @@ class PlaylistRecord(Record):
arr = array.array('L', order) arr = array.array('L', order)
# the array module doesn't directly support endianness, so we detect # the array module doesn't directly support endianness, so we detect
# the machine's endianness and swap if it is big-endian # the machine's endianness and swap if it is big-endian
if array.array('L', [1]).tobytes()[3] == 1: if array.array('L', [1]).tobytes()[3] == 1: arr.byteswap()
arr.byteswap() mhod.add(arr)
data = bytes(arr)
mhod.add(data)
self.add(mhod) self.add(mhod)
def set_playlist(self, track_ids): def set_playlist(self, track_ids): [self.add(PlaylistItemRecord(i+1, track_ids[i]), 0) for i in range(len(track_ids))]
for i in range(len(track_ids)):
self.add(PlaylistItemRecord(i+1, track_ids[i]), 0)
################################################################################
## the toplevel ITDB class ##
################################################################################
class iTunesDB: class iTunesDB:
def __init__(self, tracklist, name="Unnamed", dbid=None, dbversion=0x19): def __init__(self, tracklist, name="Unnamed", dbid=None):
if not dbid: dbid = random.randrange(0, 18446744073709551615) if not dbid: dbid = random.randrange(0, 0xffffffffffffffff) # random 64 bit integer
self.mhbd = Record(( self.mhbd = Record((
F_Tag(b"mhbd"), F_Tag(Tags.BD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(0), F_Int32(0),
F_Int32(dbversion), F_Int32(0x19),
F_ChildCount(), F_ChildCount(),
F_Int64(dbid), F_Int64(dbid),
F_Int16(2), F_Int16(2),
F_Padding(14), F_Padding(14),
F_Int16(0), # hash indicator (set later by hash58) F_Int16(0), # hash indicator (set later by hash58)
F_Padding(20), # first hash F_Padding(20), # first hash
F_Tag(b"en"), # language = 'en' F_Bytes(b"en"), # language = 'en'
F_Tag(b"\0rePear!"), # library persistent ID F_Bytes(b"\0rePear!"), # library persistent ID
F_Padding(20), # hash58 F_Padding(20), # hash58
F_Padding(80) F_Padding(80)
)) ))
self.mhsd = Record(( self.mhsd = Record((
F_Tag(b"mhsd"), F_Tag(Tags.SD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(1), F_Int32(1),
F_Padding(80) F_Padding(80)
)) ))
self.mhlt = Record(( self.mhlt = Record((
F_Tag(b"mhlt"), F_Tag(Tags.LT),
F_HeaderLength(), F_HeaderLength(),
F_ChildCount(), F_ChildCount(),
F_Padding(80) F_Padding(80)
)) ))
for track in tracklist: for track in tracklist: self.mhlt.add(TrackItemRecord(track))
self.mhlt.add(TrackItemRecord(track))
self.mhsd.add(self.mhlt) self.mhsd.add(self.mhlt)
del self.mhlt del self.mhlt
self.mhbd.add(self.mhsd) self.mhbd.add(self.mhsd)
self.mhsd = Record(( self.mhsd = Record((
F_Tag(b"mhsd"), F_Tag(Tags.SD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(2), F_Int32(2),
F_Padding(80) F_Padding(80)
)) ))
self.mhlp = Record(( self.mhlp = Record((
F_Tag(b"mhlp"), F_Tag(Tags.LP),
F_HeaderLength(), F_HeaderLength(),
F_ChildCount(), F_ChildCount(),
F_Padding(80) F_Padding(80)
@@ -426,12 +414,10 @@ class iTunesDB:
del self.mhlp del self.mhlp
self.mhbd.add(self.mhsd) self.mhbd.add(self.mhsd)
del self.mhsd del self.mhsd
result = self.mhbd.__bytes__() result = bytes(self.mhbd)
del self.mhbd del self.mhbd
return result return result
################################################################################ ################################################################################
## ArtworkDB / PhotoDB record classes ## ## ArtworkDB / PhotoDB record classes ##
################################################################################ ################################################################################
@@ -448,7 +434,6 @@ class RGB565_LE:
res[io|1] = (ord(data[ii]) & 0xF8) | (g >> 3) res[io|1] = (ord(data[ii]) & 0xF8) | (g >> 3)
io += 2 io += 2
return str(res) return str(res)
convert = staticmethod(convert)
ImageFormats = { ImageFormats = {
'nano': ((1027, 100, 100, RGB565_LE), 'nano': ((1027, 100, 100, RGB565_LE),
@@ -491,38 +476,27 @@ class ArtworkFormat:
# check if the cache file can be used # check if the cache file can be used
try: try:
s = os.stat(self.fullname) s = os.stat(self.fullname)
use_cache = stat.S_ISREG(s[stat.ST_MODE]) \ 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])
and compare_mtime(cache_info[0], s[stat.ST_MTIME]) \ except OSError: use_cache = False
and (s[stat.ST_SIZE] == cache_info[1])
except OSError:
use_cache = False
# load the cache # load the cache
if use_cache: if use_cache:
try: try:
f = open(self.fullname, "rb") with open(self.fullname, "rb") as f: self.cache = f.read()
self.cache = f.read() except IOError: use_cache = False
f.close() if not use_cache: self.cache = None
except IOError:
use_cache = False
if not use_cache:
self.cache = None
# open the destination file try: self.f = open(self.fullname, "wb")
try: except IOError:
self.f = open(self.fullname, "wb")
except IOError as e:
log("WARNING: Error opening the artwork data file `%s'\n", self.filename) log("WARNING: Error opening the artwork data file `%s'\n", self.filename)
self.f = None self.f = None
def close(self): def close(self):
if self.f: if self.f: self.f.close()
self.f.close()
try: try:
s = os.stat(self.fullname) s = os.stat(self.fullname)
cache_info = (s[stat.ST_MTIME], s[stat.ST_SIZE]) cache_info = (s[stat.ST_MTIME], s[stat.ST_SIZE])
except OSError: except OSError: cache_info = (0, 0)
cache_info = (0, 0)
return (self.fid, cache_info) return (self.fid, cache_info)
def GenerateImage(self, image, index, cache_entry=None): def GenerateImage(self, image, index, cache_entry=None):
@@ -559,10 +533,8 @@ class ArtworkFormat:
assert self.f assert self.f
self.f.seek(self.size * index) self.f.seek(self.size * index)
self.f.write(data) self.f.write(data)
except IOError: except IOError: log(" [WRITE ERROR]", True)
log(" [WRITE ERROR]", True)
# return image metadata
iinfo = ImageInfo() iinfo = ImageInfo()
iinfo.format = self iinfo.format = self
iinfo.index = index iinfo.index = index
@@ -572,19 +544,15 @@ class ArtworkFormat:
iinfo.my = my iinfo.my = my
return iinfo return iinfo
class ArtworkDBStringDataObject(Record): class ArtworkDBStringDataObject(Record):
def __init__(self, mhod_type, content): def __init__(self, mhod_type, content):
if isinstance(content, bytes): if isinstance(content, bytes): content = content.decode(sys.getfilesystemencoding(), 'replace')
content = content.decode(sys.getfilesystemencoding(), 'replace') elif not isinstance(content, str): content = str(content)
elif not isinstance(content, str):
content = str(content)
content = content.encode('utf_16_le', 'replace') content = content.encode('utf_16_le', 'replace')
padding = len(content) % 4 padding = len(content) % 4
if padding: padding = 4 - padding if padding: padding = 4 - padding
Record.__init__(self, ( super().__init__((
F_Tag(b"mhod"), F_Tag(Tags.OD),
F_Int32(0x18), F_Int32(0x18),
F_TotalLength(), F_TotalLength(),
F_Int16(mhod_type), F_Int16(mhod_type),
@@ -595,14 +563,13 @@ class ArtworkDBStringDataObject(Record):
F_Int32(0) F_Int32(0)
)) ))
self.add(content) self.add(content)
if padding: if padding: self.add("\0" * padding)
self.add("\0" * padding)
class ImageDataObject(Record): class ImageDataObject(Record):
def __init__(self, iinfo): def __init__(self, iinfo):
Record.__init__(self, ( super().__init__( (
F_Tag(b"mhod"), F_Tag(Tags.OD),
F_Int32(0x18), F_Int32(0x18),
F_TotalLength(), F_TotalLength(),
F_Int32(2), F_Int32(2),
@@ -610,7 +577,7 @@ class ImageDataObject(Record):
)) ))
mhni = Record(( mhni = Record((
F_Tag(b"mhni"), F_Tag(Tags.NI),
F_Int32(0x4C), F_Int32(0x4C),
F_TotalLength(), F_TotalLength(),
F_ChildCount(), F_ChildCount(),
@@ -626,15 +593,14 @@ class ImageDataObject(Record):
F_Padding(32) F_Padding(32)
)) ))
mhod = ArtworkDBStringDataObject(3, ":" + iinfo.format.filename) mhni.add(ArtworkDBStringDataObject(3, ":" + iinfo.format.filename))
mhni.add(mhod)
self.add(mhni) self.add(mhni)
class ImageItemRecord(Record): class ImageItemRecord(Record):
def __init__(self, img_id, dbid, iinfo_list, orig_size=0): def __init__(self, img_id, dbid, iinfo_list, orig_size=0):
Record.__init__(self, ( super().__init__( (
F_Tag(b"mhii"), F_Tag(Tags.II),
F_Int32(0x98), F_Int32(0x98),
F_TotalLength(), F_TotalLength(),
F_ChildCount(), F_ChildCount(),
@@ -644,37 +610,31 @@ class ImageItemRecord(Record):
F_Int32(orig_size), F_Int32(orig_size),
F_Padding(100) 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=({}, {})): def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
while isinstance(ImageFormats.get(model, None), str): while isinstance(ImageFormats.get(model, None), str): model = ImageFormats[model]
model = ImageFormats[model] if not model in ImageFormats: return None
if not model in ImageFormats:
return None
format_cache, image_cache = cache_data format_cache, image_cache = cache_data
formats = [] formats = []
for descriptor in ImageFormats[model]: for descriptor in ImageFormats[model]:
formats.append(ArtworkFormat(descriptor, formats.append(ArtworkFormat(descriptor, cache_info = format_cache.get(descriptor[0], (0,0))))
cache_info = format_cache.get(descriptor[0], (0,0))))
# if there's at least one format whose image file isn't cache-clean, # if there's at least one format whose image file isn't cache-clean,
# invalidate the cache # invalidate the cache
if not formats[-1].cache: if not formats[-1].cache: image_cache = {}
image_cache = {}
# Image List # Image List
mhsd = Record(( mhsd = Record((
F_Tag(b"mhsd"), F_Tag(Tags.SD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(1), F_Int32(1),
F_Padding(80) F_Padding(80)
)) ))
mhli = Record(( mhli = Record((
F_Tag(b"mhli"), F_Tag(Tags.LI),
F_HeaderLength(), F_HeaderLength(),
F_ChildCount(), F_ChildCount(),
F_Padding(80) F_Padding(80)
@@ -689,8 +649,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
log(source, False) log(source, False)
# stat this image # stat this image
try: try: s = os.stat(source)
s = os.stat(source)
except OSError as e: except OSError as e:
log(" [Error: %s]\n" % e.strerror, True) log(" [Error: %s]\n" % e.strerror, True)
continue continue
@@ -746,7 +705,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
# Date File Header # Date File Header
mhfd = Record(( mhfd = Record((
F_Tag(b"mhfd"), F_Tag(Tags.FD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(0), F_Int32(0),
@@ -764,14 +723,14 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
# Album List (dummy) # Album List (dummy)
mhsd = Record(( mhsd = Record((
F_Tag(b"mhsd"), F_Tag(Tags.SD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(2), F_Int32(2),
F_Padding(80) F_Padding(80)
)) ))
mhsd.add(Record(( mhsd.add(Record((
F_Tag(b"mhla"), F_Tag(Tags.LA),
F_HeaderLength(), F_HeaderLength(),
F_Int32(0), F_Int32(0),
F_Padding(80) F_Padding(80)
@@ -780,7 +739,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
# File List # File List
mhsd = Record(( mhsd = Record((
F_Tag(b"mhsd"), F_Tag(Tags.SD),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(3), F_Int32(3),
@@ -788,7 +747,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
)) ))
mhlf = Record(( mhlf = Record((
F_Tag(b"mhlf"), F_Tag(Tags.LF),
F_HeaderLength(), F_HeaderLength(),
F_Int32(len(formats)), F_Int32(len(formats)),
F_Padding(80) F_Padding(80)
@@ -796,7 +755,7 @@ def ArtworkDB(model, imagelist, base_id=0x40, cache_data=({}, {})):
for format in formats: for format in formats:
mhlf.add(Record(( mhlf.add(Record((
F_Tag(b"mhif"), F_Tag(Tags.IF),
F_HeaderLength(), F_HeaderLength(),
F_TotalLength(), F_TotalLength(),
F_Int32(0), F_Int32(0),
@@ -834,91 +793,67 @@ class InvalidFormat(Exception): pass
class DatabaseReader: class DatabaseReader:
def __init__(self, f="iPod_Control/iTunes/iTunesDB"): def __init__(self, f="iPod_Control/iTunes/iTunesDB"):
if isinstance(f, str): if isinstance(f, str): f = open(f, "rb")
f = open(f, "rb")
self.f = f self.f = f
self._skip_header("mhbd") self._skip_header(Tags.BD)
while True: while True:
h = self._skip_header("mhsd") h = self._skip_header(Tags.SD)
if len(h) < 16: if len(h) < 16: raise InvalidFormat
raise InvalidFormat
size, mhsd_type = struct.unpack('<LL', h[8:16]) size, mhsd_type = struct.unpack('<LL', h[8:16])
if mhsd_type == 1: if mhsd_type == 1: break # found the mhlt entry -> yeah!
break # found the mhlt entry -> yeah! if size < len(h): raise InvalidFormat
if size < len(h):
raise InvalidFormat
self.f.seek(size - len(h), 1) 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) hh = self.f.read(8)
if (len(hh) != 8) or (hh[:4] != tag): if (len(hh) != 8) or (hh[:4] != tag.value.encode()): raise InvalidFormat
raise InvalidFormat
size = struct.unpack('<L', hh[4:])[0] size = struct.unpack('<L', hh[4:])[0]
if size < 8: if size < 8: raise InvalidFormat
raise InvalidFormat
return hh + self.f.read(size - 8) return hh + self.f.read(size - 8)
def __iter__(self): return self def __iter__(self): return self
def next(self): def next(self):
try: try: header = self._skip_header(Tags.IT)
header = self._skip_header("mhit") except (IOError, InvalidFormat): raise StopIteration
except (IOError, InvalidFormat):
raise StopIteration
data_size = struct.unpack('<L', header[8:12])[0] - len(header) data_size = struct.unpack('<L', header[8:12])[0] - len(header)
if data_size<0: if data_size<0: raise InvalidFormat
raise InvalidFormat
info = {} info = {}
data = self.f.read(data_size) data = self.f.read(data_size)
if len(data) < 48: if len(data) < 48: raise InvalidFormat
raise InvalidFormat
trk = struct.unpack('<L', header[44:48])[0] trk = struct.unpack('<L', header[44:48])[0]
if trk: info['track number'] = trk if trk: info['track number'] = trk
# walk through mhods # 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]) size, mhod_type = struct.unpack('<LL', data[8:16])
value = str(data[40:size], "utf_16_le", 'replace') value = str(data[40:size], "utf_16_le", 'replace')
if mhod_type in mhod_type_map: if mhod_type in mhod_type_map: info[mhod_type_map[mhod_type]] = value
info[mhod_type_map[mhod_type]] = value
data = data[size:] data = data[size:]
return info return info
################################################################################
## Play Counts file reader ##
################################################################################
class PlayCountsItem: class PlayCountsItem:
def __init__(self, data, index): def __init__(self, data, index):
self.index = index self.index = index
self.play_count, \ self.play_count, t_last_played, self.bookmark, \
t_last_played, \ self.rating, dummy, self.skip_count, \
self.bookmark, \ t_last_skipped = struct.unpack("<LLLLLLL", data + b"\0" * (28 - len(data)))
self.rating, \
dummy, \
self.skip_count, \
t_last_skipped = \
struct.unpack("<LLLLLLL", data + "\0" * (28 - len(data)))
self.last_played = mactime2unixtime(t_last_played) self.last_played = mactime2unixtime(t_last_played)
self.last_skipped = mactime2unixtime(t_last_skipped) self.last_skipped = mactime2unixtime(t_last_skipped)
class PlayCountsReader: class PlayCountsReader:
def __init__(self, f="iPod_Control/iTunes/Play Counts"): def __init__(self, f="iPod_Control/iTunes/Play Counts"):
if isinstance(f, str): if isinstance(f, str): f = open(f, "rb")
f = open(f, "rb")
self.f = f self.f = f
self.f.seek(0, 2) self.f.seek(0, 2)
self.file_size = self.f.tell() self.file_size = self.f.tell()
self.f.seek(0) self.f.seek(0)
if self.file_size < 16: if self.file_size < 16: raise InvalidFormat
raise InvalidFormat if self.f.read(4) != b"mhdp": raise InvalidFormat
if self.f.read(4) != "mhdp":
raise InvalidFormat
header_size, self.entry_size, self.entry_count = struct.unpack("<LLL", f.read(12)) 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): if self.file_size != (header_size + self.entry_size * self.entry_count): raise InvalidFormat
raise InvalidFormat
self.f.seek(header_size) self.f.seek(header_size)
self.index = 0 self.index = 0
@@ -929,50 +864,15 @@ class PlayCountsReader:
self.index += 1 self.index += 1
return PlayCountsItem(data[:28], 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): def GenerateIDs(tracklist):
trackid = random.randint(0, (0xFFFF-0x1337) - len(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: for track in tracklist:
track['id'] = trackid track['id'] = trackid
track['dbid'] = dbid track['dbid'] = dbid
trackid += 1 trackid += 1
dbid += 1 dbid += 1
def GuessTitleAndArtist(filename): def GuessTitleAndArtist(filename):
info = {} info = {}
filename = os.path.split(filename)[1] filename = os.path.split(filename)[1]
@@ -989,39 +889,19 @@ def GuessTitleAndArtist(filename):
filename = filename[i+1:] filename = filename[i+1:]
break break
parts = filename.split(' - ', 1) parts = filename.split(' - ', 1)
if len(parts)==2: if len(parts) == 2:
info['artist'] = parts[0].strip() info['artist'] = parts[0].strip()
info['title'] = parts[1].strip(" -\r\n\t\v") info['title'] = parts[1].strip(" -\r\n\t\v")
else: else: info['title'] = filename.strip()
info['title'] = filename.strip()
return info return info
def FillMissingTitleAndArtist(track_or_list): def FillMissingTitleAndArtist(track_or_list):
if isinstance(track_or_list, list): if isinstance(track_or_list, list):
for track in track_or_list: for track in track_or_list: FillMissingTitleAndArtist(track)
FillMissingTitleAndArtist(track)
else: else:
if track_or_list.get('title',None) and track_or_list.get('artist',None): if track_or_list.get('title',None) and track_or_list.get('artist',None):
return # no need to do something, it's fine already return # no need to do something, it's fine already
guess = GuessTitleAndArtist(track_or_list['path']) guess = GuessTitleAndArtist(track_or_list['path'])
for key in ('title', 'artist', 'track number'): for key in ('title', 'artist', 'track number'):
if not(track_or_list.get(key,None)) and guess.get(key,None): if not(track_or_list.get(key,None)) and guess.get(key,None):
track_or_list[key] = guess[key] 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

View File

@@ -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.

View File

@@ -42,10 +42,6 @@ from pathlib import Path
import iTunesDB, mp3info, hash58 import iTunesDB, mp3info, hash58
Options = {} Options = {}
################################################################################
## Some internal management functions ##
################################################################################
broken_log = False broken_log = False
homedir = "" homedir = ""
logfile = None 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" + "ERROR: The iTunesDB file could not be written. This means that the iPod will\n" +
"not play anything.\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 # generate statistics
if write_ok: if write_ok:
log("\nYou can now unmount the iPod and listen to your music.\n") log("\nYou can now unmount the iPod and listen to your music.\n")