DCL 4.0
Loading...
Searching...
No Matches
TagReader.cpp
Go to the documentation of this file.
1#include <dcl/Config.h>
2
3#include <string.h> // memcmp
4
5#if __DCL_WINDOWS
6#include <windows.h>
7#endif
8
9#include <dcl/String.h>
10#include <dcl/Charset.h>
11#include <dcl/File.h>
12#include <dcl/Dir.h>
13#include <dcl/Writer.h>
14#include <dcl/Files.h>
15
16#include "ID3v1.h"
17#include "ID3v2.h"
18#include "APEv2.h"
19#include "MediaInfo.h"
20
21#include "main.h"
22#include "TagReader.h"
23
24#define __TRACE_THIS 0
25#if __TRACE_THIS
26#define __DCL_TRACE1_N __DCL_TRACE1
27#define __DCL_TRACE2_N __DCL_TRACE2
28#define __DCL_TRACE3_N __DCL_TRACE3
29#else
30#define __DCL_TRACE1_N(fmt, arg)
31#define __DCL_TRACE2_N(fmt, arg1, arg2)
32#define __DCL_TRACE3_N(fmt, arg1, arg2, arg3)
33#endif
34
35__DCL_BEGIN_NAMESPACE
36
37#if __DCL_DEBUG
38#undef __THIS_FILE__
39static const char_t __THIS_FILE__[] = __T("media/TagReader.cpp");
40#endif
41
43{
44 if (__conn) {
45 //__DCL_TRACE1(L"Connection destroy [%p]\n", __conn);
46 delete __conn;
47 }
48}
49
52{
53 __conn = NULL;
54 __dirId = 0;
55 __fileId = 0;
56
57 String driver = __args.database().substring(L"DRIVER *= *[^ ;]+", true);
58 if (!driver.isEmpty()) {
59 driver = driver.substring(driver.indexOf(L'=') + 1).trim();
60 }
61 if (!driver.isEmpty()) {
62 __conn = new SQLConnection(driver);
63 __conn->open(__args.database());
64 __args.output() << L"Database connected. ["
65 << __args.database() << L"]" << endl;
66 }
67
68 __doubleRule.assign(L'=', 80);
69 __singleRule.assign(L'-', 80);
70}
71
72void TagReader::readTag(const String& _path, FileInfo& _info)
73{
74 File file(_path);
75#if defined(__DCL_DEBUG) && __TRACE_THIS
76 {
77 File::off_t _n = file.size();
78 __DCL_TRACE2(L"[%ls] size[%lld]\n", file.path().data(), _n);
79 }
80#endif
81 char buf[160]; // 128 + 32
82
83 int id3v2Version = 0; // 0, 2, 3, 4
84 int apeVersion = 0; // 0, 1, 2
85 int id3v1Version = 0; // 0, 1
86
87 ID3v2& id3v2 = _info.id3v2;
88 APEv2& apev2 = _info.apev2;
89 ID3v1& id3v1 = _info.id3v1;
90
91 // 파일의 시작에서 ID3v2와 APEv2를 검사한다.
92 // "ID3" "APETAGEX"
93 size_t n = file.read(buf, 32);
94 if (n == 32) {
95 if (memcmp(buf, "ID3", 3) == 0) {
96 // ID3v2가 파일의 맨 앞에 있다.
97 if (_info.id3v2.read(file, buf)) {
98 id3v2Version = id3v2.version();
99 }
100 }
101 else if (memcmp(buf, "APETAGEX", 8) == 0) {
102 // APEv2가 파일의 맨 앞에 있다.
103 if (apev2.read(file, buf, 0)) {
104 apeVersion = apev2.version() / 1000;
105 }
106 }
107 }
108
109 // 파일의 끝에서 APEv2와 ID3v1을 검사한다.
110 file.seek(-160, File::END);
111 n = file.read(buf, 160);
112 if (n == 160) {
113 __DCL_TRACE2_N(L"read [%zd][%ls]\n",
114 n, String::tryString(buf, 160).data());
115 if (apeVersion == 0) {
116 if (memcmp(buf, "APETAGEX", 8) == 0) {
117 // APEv2 또는 1이 파일의 뒤쪽 ID3v1 앞에 있다.
118 if (apev2.read(file, buf, -160)) {
119 apeVersion = apev2.version() / 1000;
120 }
121 }
122 else if (memcmp(&buf[128], "APETAGEX", 8) == 0) {
123 // APEv2 또는 1이 파일의 뒤쪽에 있다.
124 if (apev2.read(file, buf, -32)) {
125 apeVersion = apev2.version() / 1000;
126 }
127 }
128 }
129
130 if (memcmp(&buf[32], "TAG", 3) == 0) {
131 if (id3v1.read(&buf[32])) {
132 id3v1Version = id3v1.version();
133 }
134 }
135 }
136}
137
138void TagReader::read(const String& _dirname, const String& _filename)
139{
140 FileInfo info(_dirname, _filename);
141 String path = _dirname + _filename;
142
143 info.size = Files::size(path);
144 time_t atime, mtime, ctime;
145 if (Files::time(path, &atime, &mtime, &ctime)) {
146 info.atime.assign(atime);
147 info.mtime.assign(mtime);
148 info.ctime.assign(ctime);
149 }
150
151 try {
152 if (_filename.toLowerCase().endsWith(L".mp3")) {
153 readTag(path, info);
154 }
155
156 MediaInfo mi(path);
157 info.inform = mi.inform_r();
158 info.format = mi.format();
159 if (mi.videoCount() > 0) {
160 info.width = mi.videoWidth(0);
161 info.height = mi.videoHeight(0);
162 info.duration = mi.videoDurationAsSeconds(0);
163 }
164 else if (mi.audioCount() > 0) {
165 info.duration = mi.audioDurationAsSeconds(0);
166 }
167 else if (mi.imageCount() > 0) {
168 info.width = mi.imageWidth(0);
169 info.height = mi.imageHeight(0);
170 }
171 }
172 catch (Exception* e) {
173 info.notes = e->toString();
174 args().errout() << e->toStringAll() << endl;
175 e->destroy();
176 }
177
178 print(info);
179
180 if (!__args.dryrun() && __conn && __conn->connected()) {
181 __conn->startTrans();
182 try {
183 persist(info);
184 __conn->commitTrans();
185 }
186 catch (Exception* e) {
187 __conn->rollbackTrans();
188 args().errout() << e->toStringAll() << endl;
189 e->destroy();
190 }
191 }
192}
193
194void TagReader::readDir(const String& _path)
195{
196 Dir dir(_path);
197 Dir::Entry entry;
198
199 // __DCL_TRACE1_N(L"[%ls]\n", dir.path().data());
200 while (dir.read(entry)) {
201 // __DCL_TRACE1_N(L"[%ls]\n", entry.toString().data());
202 String name = entry.name();
203 if (entry.isDir()) {
204 if (name.compare(L"..", name.length()) != 0) {
205 readDir(dir.path() + entry.name());
206 }
207 }
208 else {
209 read(dir.path(), name);
210 }
211 }
212}
213
214void TagReader::print(const FileInfo& _info) const
215{
216 Writer& out = args().output();
217
218 const ID3v2& id3v2 = _info.id3v2;
219 const APEv2& apev2 = _info.apev2;
220 const ID3v1& id3v1 = _info.id3v1;
221
222 out << __doubleRule << endl;
223 out << L"DIR [" << _info.dirname << L"]" << endl;
224 out << L"FILE[" << _info.filename << L"]" << endl;
225
226 out.printf(L" tags[%03d]",
227 id3v2.version() * 100 + apev2.version() / 100 + id3v1.version());
228 out << L" size[" << _info.size << L"]"
229 << L" mtime[" << _info.mtime.toString() << L"]"
230 << L" notes[" << _info.notes << L"]"
231 << endl;
232
233 out << L" format[" << _info.format << L"]"
234 << L" width[" << _info.width << L"]"
235 << L" height[" << _info.height << L"]"
236 << L" duration[" << _info.duration << L"]"
237 << endl;
238
239 if (!args().verbose()) {
240 return;
241 }
242
243 if (id3v2.version() > 0) {
244 out << __singleRule << endl;
245 out << id3v2.toString() << endl;
246
247 for (size_t i = 0; i < id3v2.frames().size(); i++) {
248 const ID3v2Frame& frame = *(ID3v2Frame*)id3v2.frames()[i];
249 out << frame.toString() << endl;
250 }
251 }
252
253 if (apev2.version() > 0) {
254 out << __singleRule << endl;
255 out << apev2.toString() << endl;
256
257 for (size_t i = 0; i < apev2.items().size(); i++) {
258 const APEv2Item& item = *(APEv2Item*)apev2.items()[i];
259 out << item.toString() << endl;
260 }
261 }
262
263 if (id3v1.version() > 0) {
264 out << __singleRule << endl;
265 out << id3v1.toString() << endl;
266 }
267
268 out << __singleRule << endl;
269 out << _info.inform << endl;
270}
271
272void TagReader::persist(const FileInfo& _info)
273{
274 Writer& out = args().output();
275 out << __singleRule << endl;
276
277 if (__dirId == 0) {
278 String sql = L""
279 "SELECT COALESCE(MAX(DIR_ID), 1000) FROM STOR_DIR"
280 ;
281 SQLQuery q(__conn);
282 q.execute(sql);
283 q.fetch();
284 __dirId = q.fields()[0].asInt32();
285 }
286
287 if (__fileId == 0) {
288 String sql = L""
289 "SELECT COALESCE(MAX(FILE_ID), 10000) FROM STOR_FILE"
290 ;
291 SQLQuery q(__conn);
292 q.execute(sql);
293 q.fetch();
294 __fileId = q.fields()[0].asInt64();
295 }
296
297 if (__dirname != _info.dirname) {
298 __dirname = _info.dirname;
299 __dirId++;
300
301 String sql = L""
302 "INSERT INTO STOR_DIR (DIR_ID, PATH, CREA_TS, MODI_TS)"
303 "\n VALUES(:DIR_ID, :PATH, CURRENT_TIMESTAMP_(), CURRENT_TIMESTAMP_())"
304 ;
305 SQLQuery q(__conn);
306 q.prepare(sql);
307 q.params()[0].setValue(__dirId);
308 q.params()[1].setValue(__dirname);
309 q.execute();
310
311 out << L" INSERT STOR_DIR [" << __dirId << L"]["
312 << __dirname << L"]" << endl;
313 }
314
315 __fileId++;
316
317 String sql = L""
318 "INSERT INTO STOR_FILE ("
319 "FILE_ID, NAME, USER_ID, STOR_ID, TYPE_ID"
320 ", DIR_ID, FILE_NM, FILE_SZ, FILE_AT, FILE_MT, FILE_CT, FILE_ER"
321 ", MEDI_FM, MEDI_WI, MEDI_HE, MEDI_DU, MEDI_NF"
322 ", READ_CO, CREA_TS, MODI_TS"
323 ")"
324 "\nVALUES ("
325 ":FILE_ID, :NAME, 0, 0, 0"
326 ", :DIR_ID, :FILE_NM, :FILE_SZ, :FILE_AT, :FILE_MT, :FILE_CT, :FILE_ER"
327 ", :MEDI_FM, :MEDI_WI, :MEDI_HE, :MEDI_DU, :MEDI_NF"
328 ", 0, CURRENT_TIMESTAMP_(), CURRENT_TIMESTAMP_()"
329 ")";
330 SQLQuery q(__conn);
331 q.prepare(sql);
332 q.params().byName(L"FILE_ID").setValue(__fileId);
333 q.params().byName(L"NAME").setValue(_info.filename);
334 q.params().byName(L"DIR_ID").setValue(__dirId);
335 q.params().byName(L"FILE_NM").setValue(_info.filename);
336 q.params().byName(L"FILE_SZ").setValue(_info.size);
337 q.params().byName(L"FILE_AT").setValue(_info.atime);
338 q.params().byName(L"FILE_MT").setValue(_info.mtime);
339 q.params().byName(L"FILE_CT").setValue(_info.ctime);
340 q.params().byName(L"FILE_ER").setValue(_info.notes);
341 q.params().byName(L"MEDI_FM").setValue(_info.format);
342 q.params().byName(L"MEDI_WI").setValue(_info.width);
343 q.params().byName(L"MEDI_HE").setValue(_info.height);
344 q.params().byName(L"MEDI_DU").setValue(_info.duration);
345 q.params().byName(L"MEDI_NF").setValue(_info.inform);
346 q.execute();
347
348 out << L" INSERT STOR_FILE [" << __fileId << L"]["
349 << _info.filename << L"]" << endl;
350
351 if (_info.id3v1.version() > 0) {
352 if (_info.id3v1.title().length() > 0) {
353 String sql = L""
354 "INSERT INTO ID3V1 ("
355 "FILE_ID, TITLE, ARTIST, ALBUM, YEAR_"
356 ", COMMENT_, TRACK, GENRE"
357 ")"
358 "\nVALUES ("
359 ":FILE_ID, :TITLE, :ARTIST, :ALBUM, :YEAR_"
360 ", :COMMENT_, :TRACK, :GENRE"
361 ")";
362 SQLQuery q(__conn);
363 q.prepare(sql);
364 q.params().byName(L"FILE_ID").setValue(__fileId);
365 q.params().byName(L"TITLE").setValue(_info.id3v1.title());
366
367 if (_info.id3v1.artist().isEmpty())
368 q.params().byName(L"ARTIST").setNull();
369 else
370 q.params().byName(L"ARTIST").setValue(_info.id3v1.artist());
371
372 if (_info.id3v1.album().isEmpty())
373 q.params().byName(L"ALBUM").setNull();
374 else
375 q.params().byName(L"ALBUM").setValue(_info.id3v1.album());
376
377 if (_info.id3v1.year().isEmpty())
378 q.params().byName(L"YEAR_").setNull();
379 else
380 q.params().byName(L"YEAR_").setValue(_info.id3v1.year());
381
382 if (_info.id3v1.comment().isEmpty())
383 q.params().byName(L"COMMENT_").setNull();
384 else
385 q.params().byName(L"COMMENT_").setValue(_info.id3v1.comment());
386
387 q.params().byName(L"TRACK").setValue(_info.id3v1.track());
388 q.params().byName(L"GENRE").setValue(_info.id3v1.genre());
389 q.execute();
390
391 if (__args.verbose()) {
392 out << L" INSERT ID3V1" << endl;
393 }
394 }
395 else {
396 args().errout() << L"Title is NULL "
397 << _info.id3v1.toString()
398 << L" [" << _info.dirname << _info.filename << L"]" << endl;
399 }
400 }
401
402 if (_info.id3v2.version() > 0) {
403 String sql = L""
404 "INSERT INTO ID3V2 ("
405 "FILE_ID, VERSION, FLAGS, SIZE_, FSBITS"
406 ")"
407 "\nVALUES ("
408 ":FILE_ID, :VERSION, :FLAGS, :SIZE_, :FSBITS"
409 ")";
410 SQLQuery q(__conn);
411 q.prepare(sql);
412 q.params().byName(L"FILE_ID").setValue(__fileId);
413 q.params().byName(L"VERSION").setValue(_info.id3v2.version());
414 q.params().byName(L"FLAGS").setValue(_info.id3v2.flags());
415 q.params().byName(L"SIZE_").setValue(_info.id3v2.size());
416 q.params().byName(L"FSBITS").setValue(_info.id3v2.fsbits());
417 q.execute();
418
419 sql = L""
420 "INSERT INTO ID3V2_FRAME ("
421 "FILE_ID, NO_, FRAME_ID, SIZE_, FLAGS"
422 ", ENCODING, TYPE_, URL, DESCRIPTION, TEXT_, BINARY_"
423 ")"
424 "\nVALUES ("
425 ":FILE_ID, :NO_, :FRAME_ID, :SIZE_, :FLAGS"
426 ", :ENCODING, :TYPE_, :URL, :DESCRIPTION, :TEXT_, :BINARY_"
427 ")";
428 q.prepare(sql);
429 for (size_t i = 0; i < _info.id3v2.frames().size(); i++) {
430 ID3v2Frame& frame = *(ID3v2Frame*)_info.id3v2.frames()[i];
431 q.params().byName(L"FILE_ID").setValue(__fileId);
432 q.params().byName(L"NO_").setValue((int)(i + 1));
433 q.params().byName(L"FRAME_ID").setValue(frame.id());
434 q.params().byName(L"SIZE_").setValue(frame.size());
435 q.params().byName(L"FLAGS").setValue(frame.flags());
436 q.params().byName(L"ENCODING").setValue(frame.encoding());
437 q.params().byName(L"TYPE_").setValue(frame.type());
438
439 if (frame.url().isEmpty())
440 q.params().byName(L"URL").setNull();
441 else
442 q.params().byName(L"URL").setValue(frame.url());
443
444 if (frame.description().isEmpty())
445 q.params().byName(L"DESCRIPTION").setNull();
446 else
447 q.params().byName(L"DESCRIPTION").setValue(frame.description());
448
449 if (frame.text().isEmpty())
450 q.params().byName(L"TEXT_").setNull();
451 else
452 q.params().byName(L"TEXT_").setValue(frame.text());
453
454 if (frame.binary().length() == 0 || 100 < frame.binary().length())
455 // APIC, PIC 제외
456 q.params().byName(L"BINARY_").setNull();
457 else
458 q.params().byName(L"BINARY_").setValue(frame.binary());
459
460 q.execute();
461 }
462
463 if (__args.verbose()) {
464 out << L" INSERT ID3V2 ID3V2_FRAME["
465 << _info.id3v2.frames().size() << L"]" << endl;
466 }
467 }
468
469 if (_info.apev2.version() > 0) {
470 String sql = L""
471 "INSERT INTO APE ("
472 "FILE_ID, VERSION, SIZE_, COUNT_, FLAGS"
473 ")"
474 "\nVALUES ("
475 ":FILE_ID, :VERSION, :SIZE_, :COUNT_, :FLAGS"
476 ")";
477 SQLQuery q(__conn);
478 q.prepare(sql);
479 q.params().byName(L"FILE_ID").setValue(__fileId);
480 q.params().byName(L"VERSION").setValue(_info.apev2.version());
481 q.params().byName(L"SIZE_").setValue(_info.apev2.size());
482 q.params().byName(L"COUNT_").setValue(_info.apev2.count());
483 q.params().byName(L"FLAGS").setValue(_info.apev2.flags());
484 q.execute();
485
486 sql = L""
487 "INSERT INTO APE_ITEM ("
488 "FILE_ID, NO_, SIZE_, FLAGS"
489 ", KEY_, VALUE_"
490 ")"
491 "\nVALUES ("
492 ":FILE_ID, :NO_, :SIZE_, :FLAGS"
493 ", :KEY_, :VALUE_"
494 ")";
495 q.prepare(sql);
496 for (size_t i = 0; i < _info.apev2.items().size(); i++) {
497 APEv2Item& item = *(APEv2Item*)_info.apev2.items()[i];
498 q.params().byName(L"FILE_ID").setValue(__fileId);
499 q.params().byName(L"NO_").setValue((int)(i + 1));
500 q.params().byName(L"SIZE_").setValue(item.size());
501 q.params().byName(L"FLAGS").setValue(item.flags());
502 q.params().byName(L"KEY_").setValue(item.key());
503
504 if (item.value().isEmpty())
505 q.params().byName(L"VALUE_").setNull();
506 else
507 q.params().byName(L"VALUE_").setValue(item.value());
508
509 q.execute();
510 }
511
512 if (__args.verbose()) {
513 out << L" INSERT APE APE_ITEM["
514 << _info.apev2.items().size() << L"]" << endl;
515 }
516 }
517}
518
519__DCL_END_NAMESPACE
#define __THIS_FILE__
Definition _trace.h:14
#define NULL
Definition Config.h:340
wchar_t char_t
Definition Config.h:275
#define __DCL_THROWS2(e1, e2)
Definition Config.h:168
#define __DCL_TRACE2_N(fmt, arg1, arg2)
#define __T(str)
Definition Object.h:44
#define __DCL_TRACE2(fmt, arg1, arg2)
Definition Object.h:377
#define endl
if(!__handle->open(bs, bs.length()))
ByteBuffer * buf
void CharsetConvertException *size_t n
Definition SQLField.cpp:253
String sql
Definition SQLQuery.cpp:289
Definition APEv2.h:16
void assign(time_t time)
Definition DateTime.cpp:723
String toString() const
Definition DateTime.cpp:826
Definition Dir.h:38
virtual String toString() const
Definition Exception.cpp:40
virtual void destroy()
Definition Exception.cpp:74
String toStringAll() const
Definition Exception.cpp:45
Definition File.h:38
@ END
Definition File.h:207
off_t seek(off_t _offset, int _whence) __DCL_THROWS1(IOException *)
Definition File.cpp:590
virtual size_t read(void *_buf, size_t _n) __DCL_THROWS1(IOException *)
Definition File.cpp:476
const String & path() const
Definition File.h:247
off_t size() const __DCL_THROWS1(IOException *)
Definition File.cpp:639
static bool time(const String &_path, time_t *_atime, time_t *_mtime, time_t *_ctime)
Definition Files.cpp:141
static uint64_t size(const String &_path) __DCL_THROWS1(IOException *)
Definition Files.cpp:161
Definition ID3v1.h:13
uint32_t size() const
Definition ID3v2.h:146
char type() const
Definition ID3v2.h:158
short flags() const
Definition ID3v2.h:150
const String & url() const
Definition ID3v2.h:162
const String id() const
Definition ID3v2.h:142
char encoding() const
Definition ID3v2.h:154
const String & description() const
Definition ID3v2.h:166
const String & text() const
Definition ID3v2.h:170
const ByteString & binary() const
Definition ID3v2.h:174
virtual String toString() const
Definition ID3v2.cpp:596
Definition ID3v2.h:16
unsigned videoCount() const
unsigned imageHeight(size_t streamNumber) const
uint32_t audioDurationAsSeconds(size_t streamNumber) const
String inform_r() const
Definition MediaInfo.cpp:88
String format() const
unsigned videoWidth(size_t streamNumber) const
unsigned imageCount() const
uint32_t videoDurationAsSeconds(size_t streamNumber) const
unsigned videoHeight(size_t streamNumber) const
unsigned audioCount() const
unsigned imageWidth(size_t streamNumber) const
SQLParam & byName(const wchar_t *_name) _CONST __DCL_THROWS1(InvalidIndexException *)
Definition SQLQuery.cpp:157
_CONST SQLParams & params() _CONST
Definition SQL.inl:144
void prepare(const String &_sql) __DCL_THROWS2(SQLException *
_CONST SQLFields & fields() _CONST
Definition SQL.inl:139
void CharsetConvertException *void execute() __DCL_THROWS1(SQLException *)
Definition SQLQuery.cpp:307
void CharsetConvertException *void fetch() __DCL_THROWS1(SQLException *)
Definition SQLQuery.cpp:329
String __doubleRule
Definition TagReader.h:86
TagReader(const MainArguments &_args) __DCL_THROWS2(SQLDriverException *
Definition TagReader.cpp:50
SQLException *void readTag(const String &_path, FileInfo &_info)
Definition TagReader.cpp:72
void persist(const FileInfo &_info)
String __dirname
Definition TagReader.h:80
SQLConnection * __conn
Definition TagReader.h:78
int32_t __dirId
Definition TagReader.h:81
const MainArguments & args() const
Definition TagReader.h:72
String __singleRule
Definition TagReader.h:87
void read(const String &_dirname, const String &_filename)
void print(const FileInfo &_info) const
const MainArguments & __args
Definition TagReader.h:77
int64_t __fileId
Definition TagReader.h:82
void readDir(const String &_path)
const String & filename
Definition TagReader.h:42
const String & dirname
Definition TagReader.h:41