2 iPod Shuffle database access
5 - http://ipodlinux.org/ITunesDB#iTunesSD_file and further
7 Author: Artem Baguinski
10 from __future__
import with_statement
11 import struct
, os
, sys
19 def set_bigendian(self
, ignore
):
21 def set_reclen(self
, ignore
):
24 class Skip(BaseField
):
25 def __init__(self
, n
):
28 file.seek(self
.size
, os
.SEEK_CUR
)
29 def read(self
, file, dict):
31 def write(self
, file, dict):
36 class Field(BaseField
):
38 def __init__(self
, name
):
41 return dict[self
.name
]
42 def put(self
, dict, value
):
43 dict[self
.name
] = value
46 def __init__(self
, const
, check
):
51 def put(self
, dict, value
):
52 if self
.check
and value
!= self
.const
:
55 def __init__(self
, packer
, name
=None, const
=None, check
= False):
57 self
.get_size
= packer
.get_size
59 self
.value_handler
= Field
.Const(const
, check
)
60 elif name
== '%reclen%':
61 def const_later(const
):
62 self
.value_handler
= Field
.Const(const
, check
)
63 self
.set_reclen
= const_later
64 elif name
is not None:
65 self
.value_handler
= Field
.Named(name
)
66 elif callback
is not None:
67 self
.value_handler
= Field
.Callback(callback
, check
)
69 raise "Bad field parameters"
71 def read(self
, file, dict):
72 self
.put(dict, self
.unpack( file.read( self
.get_size() )))
73 def write(self
, file, dict):
74 file.write( self
.pack( self
.value_handler
.get(dict) ))
75 def put(self
, dict, val
):
76 self
.value_handler
.put(dict, val
)
78 return self
.value_handler
.get(dict)
79 def unpack(self
, str):
80 return self
.packer
.unpack(str)
82 return self
.packer
.pack(val
)
83 def set_bigendian(self
, bigendian
):
84 self
.packer
.bigendian
= bigendian
87 def __init__(self
, fmt
):
89 self
.size
= struct
.calcsize(fmt
)
90 def pack(self
,val
): return struct
.pack(self
.fmt
,val
)
91 def unpack(self
,str): return struct
.unpack(self
.fmt
,str)[0]
92 def get_size(self
): return self
.size
94 class Uint8(SimplePacker
):
95 def __init__(self
): SimplePacker
.__init
__(self
,"B")
98 # def __init__(self): Uint8.__init__(self)
99 def pack(self
, val
): return Uint8
.pack(self
, (1 if val
else 0))
100 def unpack(self
, str): return Uint8
.unpack(self
, str) != 0
103 def __init__(self
, bigendian
= LITTLE_ENDIAN
):
104 self
.bigendian
= bigendian
105 def get_size(self
): return 3
108 return struct
.pack(">I",i
)[1:4]
110 return struct
.pack("<I",i
)[0:3]
113 return struct
.unpack('>I','\x00' + s
[0:3])[0]
115 return struct
.unpack('<I',s
[0:3] + '\x00')[0]
118 def __init__(self
, bigendian
= LITTLE_ENDIAN
):
119 self
.bigendian
= bigendian
122 return struct
.pack(">i",i
)[1:4]
124 return struct
.pack("<i",i
)[0:3]
126 u
= Uint24
.unpack(self
,s
)
128 return - ((~u
+ 1) & 0xfff)
133 def __init__(self
): Int24
.__init__(self
)
134 def pack(self
, val
): return Int24
.pack(self
, (-1 if val
else 0))
135 def unpack(self
, str): return Int24
.unpack(self
, str) != 0
137 class ZeroPaddedString
:
138 def __init__(self
, len, enc
):
142 return val
.encode(self
.enc
).ljust(self
.size
,'\x00')
143 def unpack(self
, str):
144 return str.decode(self
.enc
).rstrip('\x00')
145 def get_size(self
): return self
.size
148 def __init__(self
, fields
, bigendian
):
150 reclen
= self
.get_size()
152 f
.set_bigendian(bigendian
)
155 def read(self
, file, dict = {}):
156 for f
in self
.fields
:
160 def write(self
, file, dict):
161 for f
in self
.fields
:
166 for f
in self
.fields
:
171 supported_file_types
= (".mp3", ".aa", ".m4a", ".m4b", ".m4p", ".wav")
184 def set_filename(self
, filename
):
185 self
.filename
= filename
186 if filename
.endswith((".mp3",".aa")):
188 elif filename
.endswith((".m4a", ".m4b", ".m4p")):
190 elif filename
.endswith(".wav"):
193 raise "%s: unsupported file type" % (filename
)
194 if filename
.endswith((".aa",".m4b")):
195 self
.bookmarkflag
= True
197 self
.bookmarkflag
= False
198 self
.shuffleflag
= not self
.bookmarkflag
201 s
= "%s\n vol: %d " % (self
.filename
, self
.volume
)
202 if self
.starttime
!= 0 or self
.stoptime
!= 0:
203 s
+= "%5.3fs-%5.3fs " % (self
.starttime
*0.256, self
.stoptime
*0.256)
204 if self
.bookmarkflag
:
205 bm
= self
.bookmarktime
208 s
+= "bookmark: %5.3fs " % (bm
*0.256)
211 s
+= "played: %d skipped: %d" % (self
.playcount
, self
.skippedcount
)
218 def new(cls
, filename
):
219 if Track
.old_tracks
.has_key(filename
):
220 return Track
.old_tracks
[filename
]
223 t
.set_filename(filename
)
227 def set_old_tracks(cls
, lst
):
229 for i
in xrange(len(lst
)):
231 cls
.old_tracks
[t
.filename
] = t
232 cls
.old_tracks
[i
] = t
242 return """Player state:
247 track position: %d""" % (self
.volume
, self
.shuffleflag
,
248 self
.shufflepos
, self
.trackno
, self
.trackpos
)
251 iTunesSD_hdr
= Record([
252 Field(Uint24(), 'tracks'),
253 Field(Uint24(), const
=0x010800),
254 Field(Uint24(), '%reclen%', check
=True),
258 iTunesSD_track
= Record([
259 Field(Uint24(), '%reclen%', check
=True),
261 Field(Uint24(), 'starttime'),
263 Field(Uint24(), 'stoptime'),
265 Field(Uint24(), 'volume'),
266 Field(Uint24(), 'file_type'),
268 Field(ZeroPaddedString(522, 'UTF-16-LE'), 'filename'),
269 Field(Bool8(), 'shuffleflag'),
270 Field(Bool8(), 'bookmarkflag'),
274 iTunesStats_hdr
= Record([
275 Field( Uint24(), 'tracks'),
279 iTunesStats_track
= Record([
280 Field( Uint24(), '%reclen%', check
= True),
281 Field( Int24(), 'bookmarktime'),
283 Field( Uint24(), 'playcount'),
284 Field( Uint24(), 'skippedcount')],
287 iTunesPState
= Record([
288 Field( Uint8(), 'volume' ),
289 Field( Uint24(), 'shufflepos' ),
290 Field( Uint24(), 'trackno' ),
291 Field( Bool24(), 'shuffleflag'),
292 Field( Uint24(), 'trackpos'),
296 def write_iTunesSD(self
, tracks
):
297 with
open('iTunesSD', WRITE
) as file:
298 self
.iTunesSD_hdr
.write(file, {'tracks':len(tracks
)})
300 self
.iTunesSD_track
.write(file, t
.__dict
__)
303 def read_iTunesSD(self
):
304 with
open('iTunesSD', READ
) as file:
305 num_tracks
= self
.iTunesSD_hdr
.read(file)['tracks']
307 for n
in xrange(0, num_tracks
):
309 self
.iTunesSD_track
.read(file, t
.__dict
__)
313 def write_iTunesStats(self
, tracks
):
314 with
open('iTunesStats', WRITE
) as file:
315 self
.iTunesStats_hdr
.write(file, {'tracks':len(tracks
)})
317 self
.iTunesStats_track
.write(file, t
.__dict
__)
320 def read_iTunesStats(self
, tracks
):
321 with
open('iTunesStats', READ
) as file:
322 num_tracks
= self
.iTunesStats_hdr
.read(file)['tracks']
323 if num_tracks
!= len(tracks
):
324 raise "Inconsistent number of songs in iTunesSD and iTunesStats"
326 self
.iTunesStats_track
.read(file, t
.__dict
__)
328 def write_iTunesPState(self
, pstate
):
329 mode
= 'r+b' if os
.path
.exists('iTunesPState') else WRITE
330 with
open('iTunesPState', mode
) as file:
331 self
.iTunesPState
.write(file, pstate
.__dict
__)
334 def read_iTunesPState(self
):
336 with
open('iTunesPState', READ
) as file:
337 self
.iTunesPState
.read(file, pstate
.__dict
__)
341 tracks
= self
.read_iTunesSD()
342 self
.read_iTunesStats(tracks
)
343 pstate
= self
.read_iTunesPState()
344 return (tracks
, pstate
)
346 def write_all(self
, tracks
, pstate
):
347 self
.write_iTunesSD(tracks
)
348 self
.write_iTunesStats(tracks
)
349 self
.write_iTunesPState(pstate
)
351 #####################################################################
352 if __name__
== '__main__':
357 if len(sys
.argv
) > 1:
359 start_dir
= os
.getcwd()
360 os
.chdir(sys
.argv
[1])
362 tracks
, pstate
= db
.read_all()
367 Track
.set_old_tracks( tracks
)
368 t
= Track
.new( "foo.mp3" )
370 t
= Track
.new( tracks
[0].filename
)
373 if len(sys
.argv
) > 2:
376 os
.chdir(sys
.argv
[2])
377 db
.write_all(tracks
, pstate
)