patch #7498
[mldonkey.git] / src / utils / mp3tagui / mp3_tag.ml
blobb09860459d912563f3fe2c38e9e99ebb25d9b71b
1 (**************************************************************************)
2 (* Copyright 2003, 2002 b8_bavard, b8_zoggy, , b52_simon INRIA *)
3 (* *)
4 (* This file is part of mldonkey. *)
5 (* *)
6 (* mldonkey is free software; you can redistribute it and/or modify *)
7 (* it under the terms of the GNU General Public License as published *)
8 (* by the Free Software Foundation; either version 2 of the License, *)
9 (* or (at your option) any later version. *)
10 (* *)
11 (* mldonkey is distributed in the hope that it will be useful, *)
12 (* but WITHOUT ANY WARRANTY; without even the implied warranty of *)
13 (* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)
14 (* GNU General Public License for more details. *)
15 (* *)
16 (* You should have received a copy of the GNU General Public License *)
17 (* along with mldonkey; if not, write to the Free Software *)
18 (* Foundation, Inc., 59 Temple Place, Suite 330, Boston, *)
19 (* MA 02111-1307 USA *)
20 (* *)
21 (**************************************************************************)
23 (** Reading and writing tags in MP3 files. *)
26 (** Reading and writing id3 v1.1 tags. *)
27 module Id3v1 =
28 struct
30 type tag = {
31 mutable title: string;
32 mutable artist: string;
33 mutable album: string;
34 mutable year:string;
35 mutable comment: string;
36 mutable tracknum: int;
37 mutable genre: int
40 (** Check if the given file has a id3 v1.1 tag.
41 @raise Sys_error if an error occurs while opening the file.
43 let has_tag filename =
44 let ic = open_in_bin filename in
45 let len = in_channel_length ic in
46 let res =
47 if len < 128 then false else begin
48 seek_in ic (len - 128);
49 let buffer = String.create 3 in
50 really_input ic buffer 0 3;
51 buffer = "TAG"
52 end in
53 close_in ic;
54 res
56 let read_channel ic =
57 let len = in_channel_length ic in
58 if len < 128 then raise Not_found;
59 seek_in ic (len - 128);
60 let readstring len =
61 let buf = String.create len in
62 really_input ic buf 0 len;
63 Mp3_misc.chop_whitespace buf 0 in
64 if readstring 3 <> "TAG" then raise Not_found;
65 let title = readstring 30 in
66 let artist = readstring 30 in
67 let album = readstring 30 in
68 let year = readstring 4 in
69 let comment = readstring 29 in
70 let tracknum = input_byte ic in
71 let genre = input_byte ic in
72 { title = title; artist = artist; album = album; year = year;
73 comment = comment; tracknum = tracknum; genre = genre }
75 (** Read the tags in a mp3 file.
76 @raise Not_found if the file doesn't contain tags.
77 @raise Sys_error if an error occurs while opening the file.
79 let read_tag filename =
80 let ic = open_in_bin filename in
81 try
82 let res = read_channel ic in
83 close_in ic; res
84 with x ->
85 close_in ic; raise x
87 (** Write the given tag info into the given file.
88 @raise Sys_error if an error occurs with the file.
90 let write ?(title = "") ?(artist = "") ?(album = "")
91 ?(year = "") ?(comment = "") ?(tracknum = 1) ?(genre = 0)
92 filename =
93 let oc =
94 open_out_gen [Open_wronly; Open_append; Open_binary] 0o666 filename in
95 let put s len =
96 let l = String.length s in
97 for i = 0 to min l len - 1 do output_char oc s.[i] done;
98 for i = l to len - 1 do output_byte oc 0 done in
99 output_string oc "TAG";
100 put title 30;
101 put artist 30;
102 put album 30;
103 put year 4;
104 put comment 29;
105 output_byte oc tracknum;
106 output_byte oc genre;
107 close_out oc
109 (** Write the given tag structure into the given file.
110 @raise Sys_error if an error occurs with the file.
112 let write_tag filename tag =
113 write ~title: tag.title ~artist: tag.artist ~album: tag.album
114 ~year: tag.year ~comment: tag.comment ~tracknum: tag.tracknum
115 ~genre: tag.genre filename
117 let merge t1 t2 =
118 { title = if t2.title <> "" then t2.title else t1.title;
119 artist = if t2.artist <> "" then t2.artist else t1.artist;
120 album = if t2.album <> "" then t2.album else t1.album;
121 year = if t2.year <> "" && t2.year <> "0" then t2.year else t1.year;
122 comment = if t2.comment <> "" then t2.comment else t1.comment;
123 tracknum = if t2.tracknum <> 0 then t2.tracknum else t1.tracknum;
124 genre = if t2.genre <> 0xFF then t2.genre else t1.genre }
126 let no_tag = {title = ""; artist = ""; album = ""; year = "";
127 comment = ""; tracknum = 0; genre = 0xFF }
132 module Id3v2 = struct
134 type tag = (string * string) list
136 let unsynchronization = ref false
137 let last_byte_read = ref 0
139 let input_byte ic =
140 let b = Pervasives.input_byte ic in
141 let b =
142 if b = 0 && !unsynchronization && !last_byte_read = 0xFF
143 then Pervasives.input_byte ic
144 else b in
145 last_byte_read := b;
148 let input_buffer ic len =
149 let buff = String.create len in
150 for i = 0 to len - 1 do
151 buff.[i] <- Char.chr (input_byte ic)
152 done;
153 buff
155 let input_int4 ic =
156 let b4 = input_byte ic in let b3 = input_byte ic in
157 let b2 = input_byte ic in let b1 = input_byte ic in
158 (b4 lsl 24) lor (b3 lsl 16) lor (b2 lsl 8) lor b1
160 let skip_bytes ic n =
161 for i = 1 to n do ignore(input_byte ic) done
163 let valid_header header =
164 String.sub header 0 3 = "ID3"
165 && (Char.code header.[3] = 3 || Char.code header.[3] = 4)
166 && Char.code header.[5] land 0b00111111 = 0
167 && Char.code header.[6] land 0b10000000 = 0
168 && Char.code header.[7] land 0b10000000 = 0
169 && Char.code header.[8] land 0b10000000 = 0
170 && Char.code header.[9] land 0b10000000 = 0
172 let length_header header =
173 ((Char.code header.[6] lsl 21) lor
174 (Char.code header.[7] lsl 14) lor
175 (Char.code header.[8] lsl 7) lor
176 (Char.code header.[9]))
178 let decode_framedata id data =
179 if id = "TXXX" then begin
180 if data.[0] <> '\000' then raise Not_found;
181 let datapos = 1 + String.index_from data 1 '\000' in
182 Mp3_misc.chop_whitespace data datapos
183 end else if id.[0] = 'T' then begin
184 if data.[0] <> '\000' then raise Not_found;
185 Mp3_misc.chop_whitespace data 1
186 end else
187 data
189 let read_channel ic =
191 let header = String.create 10 in
192 really_input ic header 0 10;
193 if not (valid_header header) then raise Not_found;
194 let len = length_header header in
195 let startpos = pos_in ic in
196 (* Record use of unsynchronization *)
197 unsynchronization := ((Char.code header.[5] land 0b10000000) <> 0);
198 last_byte_read := 0;
199 (* Skip extended header if present *)
200 if Char.code header.[5] land 0b01000000 <> 0 then
201 skip_bytes ic (input_int4 ic);
202 (* Collect frames *)
203 let tags = ref [] in
204 while pos_in ic < startpos + len do
205 let frameid = input_buffer ic 4 in
206 let framelen = input_int4 ic in
207 let flags1 = input_byte ic in
208 let flags2 = input_byte ic in
209 let framedata = input_buffer ic framelen in
210 if flags1 land 0b00011111 = 0 && flags2 = 0 then begin
212 tags := (frameid, decode_framedata frameid framedata) :: !tags
213 with Not_found ->
216 done;
217 List.rev !tags
218 with x ->
219 raise x
221 let read_tag filename =
222 let ic = open_in_bin filename in
224 let res = read_channel ic in
225 close_in ic; res
226 with x ->
227 close_in ic; raise x
229 let last_byte_written = ref 0
231 let output_byte oc b =
232 if !last_byte_written = 0xFF then begin
233 if b = 0 || b land 0b11100000 = 0b11100000 then
234 Pervasives.output_byte oc 0
235 end;
236 Pervasives.output_byte oc b;
237 last_byte_written := b
239 let output_int4 oc n =
240 output_byte oc (n lsr 24);
241 output_byte oc (n lsr 16);
242 output_byte oc (n lsr 8);
243 output_byte oc n
245 let output_encoded_int4 oc n =
246 Pervasives.output_byte oc (((n lsr 21) land 0x7F) lsl 24);
247 Pervasives.output_byte oc (((n lsr 14) land 0x7F) lsl 16);
248 Pervasives.output_byte oc (((n lsr 7) land 0x7F) lsl 8);
249 Pervasives.output_byte oc (n land 0x7F)
251 let output_string oc s =
252 for i = 0 to String.length s - 1 do output_byte oc (Char.code s.[i]) done
254 let output_frame oc (name, data) =
255 assert (String.length name = 4);
256 if name = "TXXX" then begin
257 output_string oc name; (* tag *)
258 output_int4 oc (String.length data + 10); (* length *)
259 output_byte oc 0; output_byte oc 0; (* null flags *)
260 output_byte oc 0; (* this is ISO Latin1 *)
261 output_string oc "Comment"; (* dummy name *)
262 output_byte oc 0; (* end of name *)
263 output_string oc data; (* data *)
264 output_byte oc 0 (* termination *)
265 end else if name.[0] = 'T' then begin
266 output_string oc name; (* tag *)
267 output_int4 oc (String.length data + 2); (* length *)
268 output_byte oc 0; output_byte oc 0; (* null flags *)
269 output_byte oc 0; (* this is ISO Latin1 *)
270 output_string oc data; (* data *)
271 output_byte oc 0 (* termination *)
272 end else begin
273 output_string oc name; (* tag *)
274 output_int4 oc (String.length data); (* length *)
275 output_byte oc 0; output_byte oc 0; (* null flags *)
276 output_string oc data (* raw data *)
279 let append_data oc filename =
280 let ic = open_in_bin filename in
282 begin try
283 let header = String.create 10 in
284 really_input ic header 0 10;
285 if not (valid_header header) then raise Not_found;
286 seek_in ic (pos_in ic + length_header header)
287 with Not_found | End_of_file ->
288 seek_in ic 0
289 end;
290 let buffer = String.create 4096 in
291 let rec copy_file () =
292 let n = input ic buffer 0 (String.length buffer) in
293 if n = 0 then () else begin output oc buffer 0 n; copy_file () end in
294 copy_file ();
295 close_in ic
296 with x ->
297 close_in ic; raise x
299 let write_tag ?src:srcname filename data =
300 let origname =
301 match srcname with
302 None ->
303 Unix2.rename filename (filename ^ ".bak"); filename ^ ".bak"
304 | Some s -> s in
306 let oc = open_out_bin filename in
307 begin try
308 (* Output header *)
309 output_string oc "ID3\003\000\128";
310 let totalsize_pos = pos_out oc in
311 output_encoded_int4 oc 0;
312 last_byte_written := 0;
313 (* Output frames *)
314 List.iter (output_frame oc) data;
315 (* Patch total size *)
316 let end_pos = pos_out oc in
317 seek_out oc totalsize_pos;
318 output_encoded_int4 oc (end_pos - totalsize_pos - 4);
319 seek_out oc end_pos;
320 (* Append old file *)
321 append_data oc origname
322 with x ->
323 close_out oc; raise x
324 end;
325 close_out oc;
326 begin match srcname with
327 None -> Sys.remove origname
328 | Some s -> ()
330 with x ->
331 begin match srcname with
332 None -> Unix2.rename origname filename
333 | Some s -> ()
334 end;
335 raise x
337 let merge t1 t2 =
338 t1 @ List.filter (fun (tag, data) -> not (List.mem_assoc tag t1)) t2
340 let no_tag = []
344 let v2_to_v1 tags =
345 { Id3v1.title =
346 (try List.assoc "TIT2" tags with Not_found -> "");
347 Id3v1.artist =
348 (try List.assoc "TPE1" tags with Not_found ->
349 try List.assoc "TPE2" tags with Not_found -> "");
350 Id3v1.album =
351 (try List.assoc "TALB" tags with Not_found -> "");
352 Id3v1.year =
353 (try List.assoc "TYEA" tags with Not_found -> "");
354 Id3v1.comment =
355 (try List.assoc "TXXX" tags with Not_found -> "");
356 Id3v1.tracknum =
357 (try int_of_string (List.assoc "TRCK" tags)
358 with Not_found | Failure _ -> 0);
359 Id3v1.genre =
360 (try let g = List.assoc "TCON" tags in
361 int_of_string (String.sub g 1 (String.length g - 2))
362 with Not_found | Failure _ | Invalid_argument _ -> 0xFF)
365 let v1_to_v2 t =
366 let tags = ref [] in
367 if t.Id3v1.genre <> 0xFF then
368 tags := ("TCON", "(" ^ string_of_int t.Id3v1.genre ^ ")") :: !tags;
369 if t.Id3v1.tracknum <> 0 then
370 tags := ("TRCK", string_of_int t.Id3v1.tracknum) :: !tags;
371 if t.Id3v1.comment <> "" then
372 tags := ("TXXX", t.Id3v1.comment) :: !tags;
373 if t.Id3v1.year <> "" && t.Id3v1.year <> "0" then
374 tags := ("TYEA", t.Id3v1.year) :: !tags;
375 if t.Id3v1.album <> "" then
376 tags := ("TALB", t.Id3v1.album) :: !tags;
377 if t.Id3v1.artist <> "" then
378 tags := ("TPE1", t.Id3v1.artist) :: !tags;
379 if t.Id3v1.title <> "" then
380 tags := ("TIT2", t.Id3v1.title) :: !tags;
381 !tags
383 let read_channel_both_v1 ic =
384 let t2 =
385 try v2_to_v1(Id3v2.read_channel ic) with Not_found -> Id3v1.no_tag in
386 let t1 =
387 try Id3v1.read_channel ic with Not_found -> Id3v1.no_tag in
388 Id3v1.merge t2 t1
390 let read_channel_both_v2 ic =
391 let t2 =
392 try Id3v2.read_channel ic with Not_found -> Id3v2.no_tag in
393 let t1 =
394 try v1_to_v2(Id3v1.read_channel ic) with Not_found -> Id3v2.no_tag in
395 Id3v2.merge t2 t1
397 let read_file_both_v1 filename =
398 let ic = open_in_bin filename in
400 let res = read_channel_both_v1 ic in
401 close_in ic; res
402 with x ->
403 close_in ic; raise x
405 let read_file_both_v2 filename =
406 let ic = open_in_bin filename in
408 let res = read_channel_both_v2 ic in
409 close_in ic; res
410 with x ->
411 close_in ic; raise x
413 let write_file_both_v1 ?src:srcname filename tag =
414 Id3v2.write_tag ?src:srcname filename (v1_to_v2 tag);
415 Id3v1.write_tag filename tag
417 let write_file_both_v2 ?src:srcname filename tag =
418 Id3v2.write_tag ?src:srcname filename tag;
419 Id3v1.write_tag filename (v2_to_v1 tag)