1 (**************************************************************************)
2 (* Copyright 2003, 2002 b8_bavard, b8_zoggy, , b52_simon INRIA *)
4 (* This file is part of mldonkey. *)
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. *)
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. *)
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 *)
21 (**************************************************************************)
23 (** Reading and writing tags in MP3 files. *)
26 (** Reading and writing id3 v1.1 tags. *)
31 mutable title
: string;
32 mutable artist
: string;
33 mutable album
: string;
35 mutable comment
: string;
36 mutable tracknum
: 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
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;
57 let len = in_channel_length
ic in
58 if len < 128 then raise Not_found
;
59 seek_in
ic (len - 128);
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
82 let res = read_channel ic in
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)
94 open_out_gen
[Open_wronly
; Open_append
; Open_binary
] 0o666 filename
in
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";
105 output_byte
oc tracknum;
106 output_byte
oc genre;
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
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
140 let b = Pervasives.input_byte ic in
142 if b = 0 && !unsynchronization && !last_byte_read = 0xFF
143 then Pervasives.input_byte ic
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)
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
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);
199 (* Skip extended header if present *)
200 if Char.code
header.[5] land 0b01000000 <> 0 then
201 skip_bytes ic (input_int4 ic);
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
221 let read_tag filename
=
222 let ic = open_in_bin filename
in
224 let res = read_channel ic in
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
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);
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 *)
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
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
->
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
299 let write_tag ?src
:srcname filename data
=
303 Unix2.rename filename
(filename ^
".bak"); filename ^
".bak"
306 let oc = open_out_bin filename
in
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;
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);
320 (* Append old file *)
321 append_data oc origname
323 close_out
oc; raise x
326 begin match srcname
with
327 None
-> Sys.remove
origname
331 begin match srcname
with
332 None
-> Unix2.rename
origname filename
338 t1
@ List.filter
(fun (tag
, data
) -> not
(List.mem_assoc tag t1
)) t2
346 (try List.assoc
"TIT2" tags with Not_found
-> "");
348 (try List.assoc
"TPE1" tags with Not_found
->
349 try List.assoc
"TPE2" tags with Not_found
-> "");
351 (try List.assoc
"TALB" tags with Not_found
-> "");
353 (try List.assoc
"TYEA" tags with Not_found
-> "");
355 (try List.assoc
"TXXX" tags with Not_found
-> "");
357 (try int_of_string
(List.assoc
"TRCK" tags)
358 with Not_found
| Failure _
-> 0);
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)
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;
383 let read_channel_both_v1 ic =
385 try v2_to_v1(Id3v2.read_channel ic) with Not_found
-> Id3v1.no_tag in
387 try Id3v1.read_channel ic with Not_found
-> Id3v1.no_tag in
390 let read_channel_both_v2 ic =
392 try Id3v2.read_channel ic with Not_found
-> Id3v2.no_tag in
394 try v1_to_v2(Id3v1.read_channel ic) with Not_found
-> Id3v2.no_tag in
397 let read_file_both_v1 filename
=
398 let ic = open_in_bin filename
in
400 let res = read_channel_both_v1 ic in
405 let read_file_both_v2 filename
=
406 let ic = open_in_bin filename
in
408 let res = read_channel_both_v2 ic in
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
)