0
|
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')
|