comparison WinampLibrary.py @ 0:af65171c2294

Read support, experimental write support (.dat only)
author brad
date Fri, 09 Nov 2012 19:10:59 -0600
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:af65171c2294
1 import os
2 import struct
3 import Media
4 import time
5 from datetime import datetime
6
7 class WinampLibrary:
8
9 base_dir = ''
10 # Fields mappings in the format:
11 # <MLMedia attribute>: {'name': <Winamp internal name>, 'type': <Winamp data type>, 'winamp_id': <Winamp field ID>}
12 field_encode = {
13 'file_name': {'name': 'filename', 'type': 12, 'winamp_id': 0},
14 'title': {'name': 'title', 'type': 3, 'winamp_id': 1},
15 'artist': {'name': 'artist', 'type': 3, 'winamp_id': 2},
16 'album': {'name': 'album', 'type': 3, 'winamp_id': 3},
17 'year': {'name': 'year', 'type': 4, 'winamp_id': 4},
18 'genre': {'name': 'genre', 'type': 3, 'winamp_id': 5},
19 'comment': {'name': 'comment', 'type': 3, 'winamp_id': 6},
20 'album_artist': {'name': 'albumartist', 'type': 3, 'winamp_id': 20},
21 'composer': {'name': 'composer', 'type': 3, 'winamp_id': 24},
22 'publisher': {'name': 'publisher', 'type': 3, 'winamp_id': 23},
23 'category': {'name': 'category', 'type': 3, 'winamp_id': 34},
24 'track': {'name': 'trackno', 'type': 4, 'winamp_id': 7},
25 'length': {'name': 'length', 'type': 11, 'winamp_id': 8},
26 'type': {'name': 'type', 'type': 4, 'winamp_id': 9},
27 'last_update': {'name': 'lastupd', 'type': 10, 'winamp_id': 10},
28 'last_play': {'name': 'lastplay', 'type': 10, 'winamp_id': 11},
29 'rating': {'name': 'rating', 'type': 4, 'winamp_id': 12},
30 'play_count': {'name': 'playcount', 'type': 4, 'winamp_id': 15},
31 'file_time': {'name': 'filetime', 'type': 10, 'winamp_id': 16},
32 'file_size': {'name': 'filesize', 'type': 4, 'winamp_id': 17},
33 'bit_rate': {'name': 'bitrate', 'type': 4, 'winamp_id': 18},
34 'disc': {'name': 'disc', 'type': 4, 'winamp_id': 19},
35 'replaygain_album_gain': {'name': 'replaygain_album_gain', 'type': 3, 'winamp_id': 21},
36 'replaygain_track_gain': {'name': 'replaygain_track_gain', 'type': 3, 'winamp_id': 22},
37 'bpm': {'name': 'bpm', 'type': 4, 'winamp_id': 25},
38 'discs': {'name': 'discs', 'type': 4, 'winamp_id': 26},
39 'tracks': {'name': 'tracks', 'type': 4, 'winamp_id': 27},
40 'is_podcast': {'name': 'ispodcast', 'type': 4, 'winamp_id': 28},
41 'podcast_channel': {'name': 'podcastchannel', 'type': 3, 'winamp_id': 29},
42 'podcast_pub_date': {'name': 'podcastpubdate', 'type': 10, 'winamp_id': 30},
43 'gracenote_file_id': {'name': 'GracenoteFileID', 'type': 3, 'winamp_id': 31},
44 'gracenote_ext_data': {'name': 'GracenoteExtData', 'type': 3, 'winamp_id': 32},
45 'lossless': {'name': 'lossless', 'type': 4, 'winamp_id': 33},
46 'codec': {'name': 'codec', 'type': 3, 'winamp_id': 35},
47 'director': {'name': 'director', 'type': 3, 'winamp_id': 36},
48 'producer': {'name': 'producer', 'type': 3, 'winamp_id': 37},
49 'width': {'name': 'width', 'type': 4, 'winamp_id': 38},
50 'height': {'name': 'height', 'type': 4, 'winamp_id': 39},
51 'tuid2': {'name': 'tuid2', 'type': 3, 'winamp_id': 14},
52 }
53 field_decode = dict((v['name'], k) for k, v in field_encode.iteritems())
54 medias = []
55
56 #def __init__(self):
57 # self.base_dir = base_dir
58
59 def files_from_path(self, path):
60 path = os.path.normpath(path)
61 if not os.path.isdir(path):
62 path = os.path.dirname(path)
63 idxfile = os.path.join(path, 'main.idx')
64 datfile = os.path.join(path, 'main.dat')
65 return idxfile, datfile
66
67 def read(self, path):
68 self.fields = {}
69 idxfile, datfile = self.files_from_path(path)
70 if not os.path.isfile(idxfile):
71 raise IOError('Could not find Winamp library index file ' + idxfile)
72 if not os.path.isfile(idxfile):
73 raise IOError('Could not find Winamp library data file ' + datfile)
74
75 idx = open(idxfile, 'rb')
76 if idx.read(8) != 'NDEINDEX':
77 idx.close()
78 raise Exception(idxfile + ' does not appear to be a valid Winamp library index file')
79 num_records, = struct.unpack('<i', idx.read(4))
80 idx.read(4) # no one seems to know what these four bytes are for
81 dat = open(datfile, 'rb')
82 for _ in range(0, num_records):
83 data = idx.read(8)
84 if len(data) < 8:
85 break
86 offset, media_id = struct.unpack('ii', data)
87 print offset, media_id
88 media = self.read_media(dat, offset)
89 if media is not None:
90 self.medias.append(media)
91 idx.close()
92 dat.close()
93
94 def write(self, path):
95 fields_sorted = sorted(self.field_encode.itervalues(), key = lambda field: field['winamp_id'])
96 self.fields = list(self.field_decode[i['name']] for i in fields_sorted)
97
98 idxfile, datfile = self.files_from_path(path)
99 idx = open(idxfile, 'wb')
100 dat = open(datfile, 'wb')
101 idx.write('NDEINDEX')
102 dat.write('NDETABLE')
103 self.last_write_offset = 0
104 self.current_write_offset = 8
105 # Number of actual media files, plus the column and index records
106 num_records = len(self.medias) + 2
107 # Write the number of records plus four mystery bytes to the index
108 idx.write(struct.pack('<i', num_records) + '\xFF\x00\x00\x00')
109
110 self.write_column_record(dat)
111 self.write_mystery_record(dat)
112
113 for media in self.medias:
114 self.write_media_record(dat, media)
115
116 def write_column_record(self, dat):
117 position = 'first'
118 n = 0
119 for field in self.fields:
120 if n == len(self.fields) - 1:
121 position = 'last'
122 self.write_field(dat, field, self.field_encode[field]['name'], position, header_record = True)
123 n += 1
124 if n == 1:
125 position = 'middle'
126
127 # Who knows what this is for?
128 def write_mystery_record(self, dat):
129 data_packed = '\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x04\x4E\x6F\x6E\x65\xAE'
130 offset_next = self.current_write_offset + len(data_packed) + 14
131 header = struct.pack('<BBiii', 255, 1, len(data_packed), offset_next, 0)
132
133 write_data = header + data_packed
134 dat.write(write_data)
135
136 self.last_write_offset = self.current_write_offset
137 self.current_write_offset += len(write_data)
138
139 data_packed = '\x00\x00\x00\x00\x0C\x00\x00\x00\x08\x66\x69\x6C\x65\x6E\x61\x6D\x65\x00'
140 offset_prev = self.last_write_offset
141 header = struct.pack('<BBiii', 0, 1, len(data_packed), 0, offset_prev)
142
143 write_data = header + data_packed
144 dat.write(write_data)
145
146 self.last_write_offset = self.current_write_offset
147 self.current_write_offset += len(write_data)
148
149 def write_media_record(self, dat, media):
150 position = 'first'
151 n = 0
152 for field in self.fields:
153 if n == len(self.fields) - 1:
154 position = 'last'
155 self.write_field(dat, field, getattr(media, field), position)
156 n += 1
157 if n == 1:
158 position = 'middle'
159
160 def write_field(self, dat, field, data, position, header_record = False):
161 if data is None:
162 return
163
164 field_id = self.field_encode[field]['winamp_id']
165 data_type = self.field_encode[field]['type']
166
167 if header_record:
168 data_packed = struct.pack('<BBB', data_type, 0, len(data))
169 data_packed += data
170 data_type = 0
171 else:
172 # String field
173 if data_type == 3 or data_type == 12:
174 data_encoded = data.encode('utf-16')
175 data_packed = struct.pack('<H', len(data_encoded)) + data_encoded
176
177 # Integer field
178 elif data_type == 4 or data_type == 11:
179 data_packed = struct.pack('<i', data)
180
181 # Date field
182 elif data_type == 10:
183 timestamp = int(time.mktime(data.timetuple()))
184 data_packed = struct.pack('<i', timestamp)
185
186 else:
187 print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + field
188
189 offset_prev = self.last_write_offset
190 offset_next = self.current_write_offset + len(data_packed) + 14
191 if position == 'first':
192 offset_prev = 0
193 elif position == 'last':
194 offset_next = 0
195
196 header = struct.pack('<BBiii', field_id, data_type, len(data_packed), offset_next, offset_prev)
197 write_data = header + data_packed
198 dat.write(write_data)
199
200 self.last_write_offset = self.current_write_offset
201 self.current_write_offset += len(write_data)
202
203 def read_media(self, dat, offset):
204 dat.seek(offset)
205 media = Media.Media()
206 while True:
207 field_id, data_type, size, offset_next, offset_prev = struct.unpack('<BBiii', dat.read(14))
208 print 'data', field_id, data_type, size, offset_next, offset_prev
209 # Column field
210 if data_type == 0:
211 # The column name is always a string, and the middle byte appears to be unused
212 _, _, data_size = struct.unpack('<BBB', dat.read(3))
213 field_name = dat.read(data_size)
214 if field_name in self.field_decode:
215 self.fields[field_id] = field_name
216 else:
217 print 'Warning: unknown field name ' + field_name
218
219 elif field_id in self.fields:
220 prop = self.field_decode[self.fields[field_id]]
221 value = None
222
223 # String field
224 if data_type == 3 or data_type == 12:
225 data_size, = struct.unpack('<H', dat.read(2))
226 data = dat.read(data_size)
227 enc = 'utf-16' if data.find('\xff\xfe') == 0 else 'ascii'
228 value = unicode(data, encoding=enc, errors='ignore')
229
230 # Integer field
231 elif data_type == 4 or data_type == 11:
232 value, = struct.unpack('<i', dat.read(4))
233
234 # Date field
235 elif data_type == 10:
236 timestamp, = struct.unpack('<i', dat.read(4))
237 value = datetime.fromtimestamp(timestamp)
238
239 # Index field? Not actually a media
240 elif data_type == 1:
241 return
242
243 else:
244 print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + self.fields[field_id]
245
246 if value is not None:
247 setattr(media, prop, value)
248
249 else:
250 print '?' + str(field_id)
251 if offset_next == 0:
252 break
253 dat.seek(offset_next)
254
255 if data_type > 1: # not column or index field
256 return media
257
258 lib = WinampLibrary()
259 lib.read('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml')
260 lib.write('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml\\out')