1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module iv
.cuefile
/*is aliced*/;
27 // ////////////////////////////////////////////////////////////////////////// //
30 static string
koi2trlocase (const(char)[] s
) {
32 foreach (char ch
; s
) {
33 if (ch
== '\xe1' || ch
== '\xc1') res
~= "a";
34 else if (ch
== '\xe2' || ch
== '\xc2') res
~= "b";
35 else if (ch
== '\xf7' || ch
== '\xd7') res
~= "v";
36 else if (ch
== '\xe7' || ch
== '\xc7') res
~= "g";
37 else if (ch
== '\xe4' || ch
== '\xc4') res
~= "d";
38 else if (ch
== '\xe5' || ch
== '\xc5') res
~= "e";
39 else if (ch
== '\xb3' || ch
== '\xa3') res
~= "yo";
40 else if (ch
== '\xf6' || ch
== '\xd6') res
~= "zh";
41 else if (ch
== '\xfa' || ch
== '\xda') res
~= "z";
42 else if (ch
== '\xe9' || ch
== '\xc9') res
~= "i";
43 else if (ch
== '\xea' || ch
== '\xca') res
~= "j";
44 else if (ch
== '\xeb' || ch
== '\xcb') res
~= "k";
45 else if (ch
== '\xec' || ch
== '\xcc') res
~= "l";
46 else if (ch
== '\xed' || ch
== '\xcd') res
~= "m";
47 else if (ch
== '\xee' || ch
== '\xce') res
~= "n";
48 else if (ch
== '\xef' || ch
== '\xcf') res
~= "o";
49 else if (ch
== '\xf0' || ch
== '\xd0') res
~= "p";
50 else if (ch
== '\xf2' || ch
== '\xd2') res
~= "r";
51 else if (ch
== '\xf3' || ch
== '\xd3') res
~= "s";
52 else if (ch
== '\xf4' || ch
== '\xd4') res
~= "t";
53 else if (ch
== '\xf5' || ch
== '\xd5') res
~= "u";
54 else if (ch
== '\xe6' || ch
== '\xc6') res
~= "f";
55 else if (ch
== '\xe8' || ch
== '\xc8') res
~= "h";
56 else if (ch
== '\xe3' || ch
== '\xc3') res
~= "c";
57 else if (ch
== '\xfe' || ch
== '\xde') res
~= "ch";
58 else if (ch
== '\xfb' || ch
== '\xdb') res
~= "sh";
59 else if (ch
== '\xfd' || ch
== '\xdd') res
~= "sch";
60 else if (ch
== '\xff' || ch
== '\xdf') {} //res ~= "x"; // tvyordyj znak
61 else if (ch
== '\xf9' || ch
== '\xd9') res
~= "y";
62 else if (ch
== '\xf8' || ch
== '\xd8') {} //res ~= "w"; // myagkij znak
63 else if (ch
== '\xfc' || ch
== '\xdc') res
~= "e";
64 else if (ch
== '\xe0' || ch
== '\xc0') res
~= "ju";
65 else if (ch
== '\xf1' || ch
== '\xd1') res
~= "ja";
66 else if (ch
>= 'A' && ch
<= 'Z') res
~= cast(char)(ch
+32);
67 else if (ch
>= 'a' && ch
<= 'z') res
~= ch
;
68 else if (ch
>= '0' && ch
<= '9') res
~= ch
;
70 if (res
.length
> 0 && res
[$-1] != '_') res
~= '_';
73 while (res
.length
&& res
[$-1] == '_') res
= res
[0..$-1];
74 if (res
.length
== 0) res
= "_";
80 string artist
; // performer
83 uint year
; // 0: unknown
85 ulong pregapmsecs
; // index 00, or startmsecs
86 ulong startmsecs
; // index 01
88 @property ulong start () const pure nothrow @safe @nogc { pragma(inline
, true); return (pregapmsecs
!= pregapmsecs
.max
&& pregapmsecs
< startmsecs ? pregapmsecs
: startmsecs
); }
92 ulong parseIndex (const(char)[] s
) {
93 import std
.algorithm
: splitter
;
95 import std
.range
: enumerate
;
98 foreach (immutable idx
, /*auto*/ sv
; s
.splitter(':').enumerate
) {
99 if (idx
>= msf
.length
) throw new Exception("invalid index");
100 lastHit
= (idx
== msf
.length
-1);
101 msf
[idx
] = sv
.to
!uint;
103 if (!lastHit
) throw new Exception("invalid index");
104 if (msf
[1] > 59) throw new Exception("invalid index");
105 if (msf
[2] > 74) throw new Exception("invalid index");
106 //return cast(uint)((((msf[1]+msf[0]*60)*75)/75.0)*1000.0);
107 return cast(uint)((((msf
[1]+msf
[0]*60)*75+msf
[2])/75.0)*1000.0);
114 uint year
; // 0: unknown
119 void clear () { this = this.init
; }
121 void load (const(char)[] fname
) { load(VFile(fname
)); }
123 void load (VFile fl
) {
125 scope(failure
) clear();
127 char lastSavedChar
= 0;
129 bool firstLine
= true;
135 if (line
.length
>= 3 && line
[0..3] == "\xEF\xBB\xBF") line
= line
[3..$]; // fuck BOM
139 if (lastSavedChar
) { linebuf
[pos
++] = lastSavedChar
; lastSavedChar
= 0; }
140 while (pos
< linebuf
.length
) {
141 auto rd
= fl
.rawRead(linebuf
[pos
..pos
+1]);
142 if (rd
.length
== 0) {
143 if (pos
== 0) { line
= null; return false; }
144 line
= linebuf
[0..pos
];
147 char ch
= linebuf
[pos
];
149 line
= linebuf
[0..pos
];
153 rd
= fl
.rawRead((&lastSavedChar
)[0..1]);
154 if (rd
.length
== 1 && lastSavedChar
== '\n') lastSavedChar
= 0;
155 line
= linebuf
[0..pos
];
160 throw new Exception("line too long!");
164 const(char)[] nextWord(bool doupper
) () {
165 while (line
.length
&& line
[0] <= ' ') line
= line
[1..$];
166 if (line
.length
== 0) return null;
169 if (line
[0] == '"') {
171 while (epos
< line
.length
&& line
[epos
] != '"') {
173 if (line
[epos
] == '\\' && line
.length
-epos
> 1) epos
+= 2; else ++epos
;
176 if (epos
< line
.length
) {
177 assert(line
[epos
] == '"');
180 line
= line
[epos
..$];
181 // remove spaces (i don't need 'em anyway; and i don't care about idiotic filenames)
182 while (res
.length
&& res
[0] <= ' ') res
= res
[1..$];
183 while (res
.length
&& res
[$-1] <= ' ') res
= res
[0..$-1];
186 while (epos
< line
.length
&& line
[epos
] > ' ') ++epos
;
188 line
= line
[epos
..$];
191 if (res
!is null && !res
.utf8Valid
) return res
.recode("utf-8", "cp1251");
192 static if (doupper
) {
196 foreach (char ch
; res
) {
197 if (ch
>= 128) { doconv
= false; break; }
198 if (ch
>= 'a' && ch
<= 'z') doconv
= true;
200 if (doconv
) foreach (ref char ch
; res
) if (ch
>= 'a' && ch
<= 'z') ch
-= 32;
207 //writeln("[", line, "]");
208 auto w
= nextWord
!true();
209 if (w
is null) continue;
211 case "REM": // special
214 case "DATE": case "YEAR":
215 w
= nextWord
!false();
217 try { import std
.conv
: to
; yr
= w
.to
!ushort(10); } catch (Exception
) {}
218 if (yr
>= 1900 && yr
<= 3000) {
219 if (tracks
.length
) tracks
[$-1].year
= yr
; else year
= yr
;
223 w
= nextWord
!false();
225 if (tracks
.length
) tracks
[$-1].genre
= w
.idup
; else genre
= w
.idup
;
231 case "TRACK": // new track
233 tracks
[$-1].pregapmsecs
= tracks
[$-1].pregapmsecs
.max
;
236 import std
.conv
: to
;
237 auto tn
= w
.to
!ubyte(10);
238 if (tn
!= tracks
.length
) throw new Exception("invalid track number");
239 } catch (Exception
) {
240 throw new Exception("fucked track number");
243 if (w
!= "AUDIO") throw new Exception("non-audio track");
246 w
= nextWord
!false();
248 if (tracks
.length
) tracks
[$-1].artist
= w
.idup
; else artist
= w
.idup
;
252 w
= nextWord
!false();
254 if (tracks
.length
) tracks
[$-1].title
= w
.idup
; else album
= w
.idup
;
258 w
= nextWord
!false();
260 if (tracks
.length
) tracks
[$-1].filename
= w
.idup
; else filename
= w
.idup
;
264 // mm:ss:ff (minute-second-frame) format. There are 75 such frames per second of audio
265 // 00: pregap, optional
267 if (tracks
.length
== 0) throw new Exception("index without track");
268 w
= nextWord
!false();
270 import std
.conv
: to
;
271 auto n
= w
.to
!ubyte(10);
274 tracks
[$-1].startmsecs
= parseIndex(w
);
277 tracks
[$-1].pregapmsecs
= parseIndex(w
);
279 } catch (Exception e
) {
280 writeln("ERROR: ", e
.msg
, " (", w
, ")");
281 throw new Exception("fucked index");
284 case "PREGAP": case "POSTGAP": break; // ignore
285 case "ISRC": case "CATALOG": case "FLAGS": case "CDTEXTFILE": break;
288 writeln("unknown CUE keyword: '", w
, "'");
289 throw new Exception("invalid keyword");
294 foreach (immutable tidx
, ref trk
; tracks
) {
295 if (trk
.pregapmsecs
== trk
.pregapmsecs
.max
) trk
.pregapmsecs
= trk
.startmsecs
;
296 if (trk
.artist
== artist
) trk
.artist
= null;
297 if (trk
.year
== year
) trk
.year
= 0;
298 if (trk
.genre
== genre
) trk
.genre
= null;
299 if (trk
.filename
== filename
) trk
.filename
= null;
301 string t
= simpleParseInt(trk
.title
, pidx
);
302 if (pidx
== tidx
+1 && t
.length
&& t
.ptr
[0] == '.') t
= t
[1..$].xstrip
;
303 if (pidx
== tidx
+1 && t
.length
) trk
.title
= t
;
307 void dump (VFile fo
) {
308 fo
.writeln("=======================");
309 if (artist
.length
) fo
.writeln("ARTIST: <", artist
.recodeToKOI8
, ">");
310 if (album
.length
) fo
.writeln("ALBUM : <", album
.recodeToKOI8
, ">");
311 if (genre
.length
) fo
.writeln("GENRE : <", genre
.recodeToKOI8
, ">");
312 if (year
) fo
.writeln("YEAR : <", year
, ">");
313 if (filename
.length
) fo
.writeln("FILE : <", filename
.recodeToKOI8
, ">");
315 fo
.writeln("TRACKS: ", tracks
.length
);
316 foreach (immutable tidx
, const ref trk
; tracks
) {
317 fo
.writefln(" TRACK #%02d: start: %d:%02d.%03d", tidx
+1, trk
.startmsecs
/1000/60, (trk
.startmsecs
/1000)%60, trk
.startmsecs
%1000);
318 if (trk
.artist
.length
) fo
.writeln(" ARTIST: <", trk
.artist
.recodeToKOI8
, ">");
319 if (trk
.title
.length
) fo
.writeln(" TITLE : <", trk
.title
.recodeToKOI8
, ">");
320 if (trk
.genre
.length
) fo
.writeln(" GENRE : <", trk
.genre
.recodeToKOI8
, ">");
321 if (trk
.year
) fo
.writeln(" YEAR : <", trk
.year
, ">");
322 if (trk
.filename
.length
) fo
.writeln(" FILE : <", trk
.filename
.recodeToKOI8
, ">");
323 if (trk
.title
.length
) fo
.writeln(" XFILE : <", koi2trlocase(trk
.title
.recodeToKOI8
), ">");
328 void dump () { dump(stdout
); }
332 // return string w/o parsed number
333 static inout(char)[] simpleParseInt (inout(char)[] src
, out int num
) nothrow @trusted @nogc {
335 while (pos
< src
.length
&& src
.ptr
[pos
] <= ' ') ++pos
;
336 if (pos
>= src
.length || src
.ptr
[pos
] < '0' || src
.ptr
[pos
] > '9') {
341 while (pos
< src
.length
) {
342 char ch
= src
.ptr
[pos
];
343 if (ch
< '0' || ch
> '9') break;
346 if (num
< onum
) { num
= -1; return src
; }
349 while (pos
< src
.length
&& src
.ptr
[pos
] <= ' ') ++pos
;