1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 // * Redistributions of source code must retain the above copyright notice,
11 // this list of conditions and the following disclaimer.
12 //
13 // * Redistributions in binary form must reproduce the above copyright
14 // notice, this list of conditions and the following disclaimer in the
15 // documentation and/or other materials provided with the distribution.
16 //
17 // * Neither the name of the author nor the names of other contributors may
18 // be used to endorse or promote products derived from this software
19 // without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34
35 #include "pch.hpp"
36
37 #include <musikcore/library/track/IndexerTrack.h>
38
39 #include <musikcore/support/Common.h>
40 #include <musikcore/support/Preferences.h>
41 #include <musikcore/support/PreferenceKeys.h>
42 #include <musikcore/db/Connection.h>
43 #include <musikcore/db/Statement.h>
44 #include <musikcore/library/LocalLibrary.h>
45 #include <musikcore/io/DataStreamFactory.h>
46
47 #include <unordered_map>
48
49 using namespace musik::core;
50 using namespace musik::core::sdk;
51
52 #define GENRE_TRACK_COLUMN_NAME "genre"
53 #define GENRES_TABLE_NAME "genres"
54 #define GENRE_TRACK_JUNCTION_TABLE_NAME "track_genres"
55 #define GENRE_TRACK_FOREIGN_KEY "genre_id"
56
57 #define ARTIST_TRACK_COLUMN_NAME "artist"
58 #define ARTISTS_TABLE_NAME "artists"
59 #define ARTIST_TRACK_JUNCTION_TABLE_NAME "track_artists"
60 #define ARTIST_TRACK_FOREIGN_KEY "artist_id"
61
62 std::mutex IndexerTrack::sharedWriteMutex;
63 static std::unordered_map<std::string, int64_t> metadataIdCache;
64 static std::unordered_map<int, int64_t> thumbnailIdCache; /* albumId:thumbnailId */
65
66 /* http://stackoverflow.com/a/2351171 */
hash32(const char * str)67 static size_t hash32(const char* str) noexcept {
68 unsigned int h;
69 unsigned char *p;
70 h = 0;
71 for (p = (unsigned char*)str; *p != '\0'; p++) {
72 h = 37 * h + *p;
73 }
74 h += (h >> 5);
75 return h;
76 }
77
OnIndexerStarted(db::Connection & dbConnection)78 void IndexerTrack::OnIndexerStarted(db::Connection &dbConnection) {
79 /* unused, for now */
80 }
81
OnIndexerFinished(db::Connection & dbConnection)82 void IndexerTrack::OnIndexerFinished(db::Connection &dbConnection) {
83 metadataIdCache.clear();
84
85 /* if we got some new album art, make sure all of the tracks for the
86 album get the updated ID! */
87 std::string query = "UPDATE tracks SET thumbnail_id=? WHERE album_id=?)";
88 db::ScopedTransaction transaction(dbConnection);
89 for (auto it : thumbnailIdCache) {
90 db::Statement stmt(query.c_str(), dbConnection);
91 stmt.BindInt64(0, it.second);
92 stmt.BindInt64(1, it.first);
93 stmt.Step();
94 }
95
96 thumbnailIdCache.clear();
97 }
98
IndexerTrack(int64_t trackId)99 IndexerTrack::IndexerTrack(int64_t trackId)
100 : internalMetadata(new IndexerTrack::InternalMetadata())
101 , trackId(trackId)
102 {
103 }
104
~IndexerTrack()105 IndexerTrack::~IndexerTrack() {
106 delete this->internalMetadata;
107 this->internalMetadata = nullptr;
108 }
109
GetString(const char * metakey)110 std::string IndexerTrack::GetString(const char* metakey) {
111 if (metakey && this->internalMetadata) {
112 MetadataMap::iterator metavalue = this->internalMetadata->metadata.find(metakey);
113 if (metavalue != this->internalMetadata->metadata.end()) {
114 return metavalue->second;
115 }
116 }
117
118 return "";
119 }
120
GetInt64(const char * key,long long defaultValue)121 long long IndexerTrack::GetInt64(const char* key, long long defaultValue) {
122 try {
123 std::string value = GetString(key);
124 if (value.size()) {
125 return std::stoll(GetString(key));
126 }
127 } catch (...) {
128 }
129 return defaultValue;
130 }
131
GetInt32(const char * key,unsigned int defaultValue)132 int IndexerTrack::GetInt32(const char* key, unsigned int defaultValue) {
133 try {
134 std::string value = GetString(key);
135 if (value.size()) {
136 return std::stol(GetString(key));
137 }
138 }
139 catch (...) {
140 }
141 return defaultValue;
142 }
143
GetDouble(const char * key,double defaultValue)144 double IndexerTrack::GetDouble(const char* key, double defaultValue) {
145 try {
146 std::string value = GetString(key);
147 if (value.size()) {
148 return std::stod(GetString(key));
149 }
150 } catch (...) {
151 }
152 return defaultValue;
153 }
154
SetValue(const char * metakey,const char * value)155 void IndexerTrack::SetValue(const char* metakey, const char* value) {
156 if (metakey && value) {
157 this->internalMetadata->metadata.insert(
158 std::pair<std::string, std::string>(metakey,value));
159 }
160 }
161
ClearValue(const char * metakey)162 void IndexerTrack::ClearValue(const char* metakey) {
163 if (this->internalMetadata) {
164 this->internalMetadata->metadata.erase(metakey);
165 }
166 }
167
Contains(const char * metakey)168 bool IndexerTrack::Contains(const char* metakey) {
169 auto md = this->internalMetadata;
170 return md && md->metadata.find(metakey) != md->metadata.end();
171 }
172
SetThumbnail(const char * data,long size)173 void IndexerTrack::SetThumbnail(const char *data, long size) {
174 if (this->internalMetadata->thumbnailData) {
175 delete[] this->internalMetadata->thumbnailData;
176 }
177
178 this->internalMetadata->thumbnailData = new char[size];
179 this->internalMetadata->thumbnailSize = size;
180
181 memcpy(this->internalMetadata->thumbnailData, data, size);
182 }
183
GetThumbnailId()184 int64_t IndexerTrack::GetThumbnailId() {
185 std::string key = this->GetString("album") + "-" + this->GetString("album_artist");
186 const size_t id = hash32(key.c_str());
187 auto it = thumbnailIdCache.find((int) id);
188 if (it != thumbnailIdCache.end()) {
189 return it->second;
190 }
191 return 0;
192 }
193
ContainsThumbnail()194 bool IndexerTrack::ContainsThumbnail() {
195 if (this->internalMetadata->thumbnailData &&
196 this->internalMetadata->thumbnailSize)
197 {
198 return true;
199 }
200 std::unique_lock<std::mutex> lock(sharedWriteMutex);
201 return this->GetThumbnailId() != 0;
202 }
203
SetReplayGain(const ReplayGain & replayGain)204 void IndexerTrack::SetReplayGain(const ReplayGain& replayGain) {
205 this->internalMetadata->replayGain.reset();
206 this->internalMetadata->replayGain = std::make_shared<ReplayGain>();
207 memcpy(this->internalMetadata->replayGain.get(), &replayGain, sizeof(ReplayGain));
208 }
209
GetReplayGain()210 ReplayGain IndexerTrack::GetReplayGain() {
211 throw std::runtime_error("not implemented");
212 }
213
GetMetadataState()214 MetadataState IndexerTrack::GetMetadataState() {
215 throw std::runtime_error("not implemented");
216 }
217
SetMetadataState(MetadataState state)218 void IndexerTrack::SetMetadataState(MetadataState state) {
219 /* not used, but needs to be stubbed because Indexer uses TrackMetadataQuery,
220 which calls this method */
221 }
222
Uri()223 std::string IndexerTrack::Uri() {
224 return this->GetString("filename");
225 }
226
GetString(const char * key,char * dst,int size)227 int IndexerTrack::GetString(const char* key, char* dst, int size) {
228 return (int) CopyString(this->GetString(key), dst, size);
229 }
230
Uri(char * dst,int size)231 int IndexerTrack::Uri(char* dst, int size) {
232 return (int) CopyString(this->Uri(), dst, size);
233 }
234
GetValues(const char * metakey)235 Track::MetadataIteratorRange IndexerTrack::GetValues(const char* metakey) {
236 if (this->internalMetadata) {
237 return this->internalMetadata->metadata.equal_range(metakey);
238 }
239
240 return Track::MetadataIteratorRange();
241 }
242
GetAllValues()243 Track::MetadataIteratorRange IndexerTrack::GetAllValues() {
244 if (this->internalMetadata) {
245 return Track::MetadataIteratorRange(
246 this->internalMetadata->metadata.begin(),
247 this->internalMetadata->metadata.end());
248 }
249
250 return Track::MetadataIteratorRange();
251 }
252
GetId()253 int64_t IndexerTrack::GetId() {
254 return this->trackId;
255 }
256
NeedsToBeIndexed(const boost::filesystem::path & file,db::Connection & dbConnection)257 bool IndexerTrack::NeedsToBeIndexed(
258 const boost::filesystem::path &file,
259 db::Connection &dbConnection)
260 {
261 try {
262 this->SetValue("path", file.string().c_str());
263 this->SetValue("filename", file.string().c_str());
264
265 size_t lastDot = file.leaf().string().find_last_of(".");
266 if (lastDot != std::string::npos) {
267 this->SetValue("extension", file.leaf().string().substr(lastDot + 1).c_str());
268 }
269
270 const size_t fileSize = (size_t) boost::filesystem::file_size(file);
271 const size_t fileTime = (size_t) boost::filesystem::last_write_time(file);
272
273 this->SetValue("filesize", std::to_string(fileSize).c_str());
274 this->SetValue("filetime", std::to_string(fileTime).c_str());
275
276 db::Statement stmt(
277 "SELECT id, filename, filesize, filetime " \
278 "FROM tracks t " \
279 "WHERE filename=?", dbConnection);
280
281 stmt.BindText(0, this->GetString("filename"));
282
283 const bool fileDifferent = true;
284
285 if (stmt.Step() == db::Row) {
286 this->trackId = stmt.ColumnInt64(0);
287 const int dbFileSize = stmt.ColumnInt32(2);
288 const int dbFileTime = stmt.ColumnInt32(3);
289
290 if (fileSize == dbFileSize && fileTime == dbFileTime) {
291 return false;
292 }
293 }
294 }
295 catch (...) {
296 }
297
298 return true;
299 }
300
writeToTracksTable(db::Connection & dbConnection,IndexerTrack & track)301 static int64_t writeToTracksTable(
302 db::Connection &dbConnection,
303 IndexerTrack& track)
304 {
305 std::string externalId = track.GetString("external_id");
306 int64_t id = track.GetId();
307
308 if (externalId.size() == 0) {
309 return 0;
310 }
311
312 const int sourceId = track.GetInt32("source_id", 0);
313
314 /* if there's no ID specified, but we have an external ID, let's
315 see if we can find the corresponding ID. this can happen when
316 IInputSource plugins are reading/writing track data. */
317 if (id == 0) {
318 db::Statement stmt("SELECT id FROM tracks WHERE source_id=? AND external_id=?", dbConnection);
319 stmt.BindInt32(0, sourceId);
320 stmt.BindText(1, externalId);
321 if (stmt.Step() == db::Row) {
322 id = stmt.ColumnInt64(0);
323 track.SetId(id);
324 }
325 }
326
327 std::string query;
328
329 if (id != 0) {
330 query =
331 "UPDATE tracks "
332 "SET track=?, disc=?, bpm=?, duration=?, filesize=?, "
333 " title=?, filename=?, filetime=?, path_id=?, "
334 " date_updated=julianday('now'), external_id=? "
335 "WHERE id=?";
336 }
337 else {
338 query =
339 "INSERT INTO tracks "
340 "(track, disc, bpm, duration, filesize, title, filename, "
341 " filetime, path_id, external_id, date_added, date_updated) "
342 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, julianday('now'), julianday('now'))";
343 }
344
345 db::Statement stmt(query.c_str(), dbConnection);
346
347 stmt.BindText(0, track.GetString("track"));
348 stmt.BindText(1, track.GetString("disc"));
349 stmt.BindText(2, track.GetString("bpm"));
350 stmt.BindInt32(3, track.GetInt32("duration"));
351 stmt.BindInt32(4, track.GetInt32("filesize"));
352 stmt.BindText(5, track.GetString("title"));
353 stmt.BindText(6, track.GetString("filename"));
354 stmt.BindInt32(7, track.GetInt32("filetime"));
355 stmt.BindInt64(8, track.GetInt64("path_id"));
356 stmt.BindText(9, track.GetString("external_id"));
357
358 if (id != 0) {
359 stmt.BindInt64(10, id);
360 }
361
362 if (stmt.Step() == db::Done) {
363 if (id == 0) {
364 return dbConnection.LastInsertedId();
365 }
366 }
367
368 return id;
369 }
370
removeRelation(db::Connection & connection,const std::string & field,int64_t trackId)371 static void removeRelation(
372 db::Connection& connection,
373 const std::string& field,
374 int64_t trackId)
375 {
376 std::string query = u8fmt("DELETE FROM %s WHERE track_id=?", field.c_str());
377 db::Statement stmt(query.c_str(), connection);
378 stmt.BindInt64(0, trackId);
379 stmt.Step();
380 }
381
removeKnownFields(Track::MetadataMap & metadata)382 static void removeKnownFields(Track::MetadataMap& metadata) {
383 metadata.erase("track");
384 metadata.erase("disc");
385 metadata.erase("bpm");
386 metadata.erase("duration");
387 metadata.erase("title");
388 metadata.erase("filename");
389 metadata.erase("filetime");
390 metadata.erase("filesize");
391 metadata.erase("title");
392 metadata.erase("path");
393 metadata.erase("extension");
394 metadata.erase("genre");
395 metadata.erase("artist");
396 metadata.erase("album_artist");
397 metadata.erase("album");
398 metadata.erase("source_id");
399 metadata.erase("external_id");
400 metadata.erase("visible");
401 }
402
SaveReplayGain(db::Connection & dbConnection)403 void IndexerTrack::SaveReplayGain(db::Connection& dbConnection)
404 {
405 auto replayGain = this->internalMetadata->replayGain;
406 if (replayGain) {
407 {
408 db::Statement removeOld("DELETE FROM replay_gain WHERE track_id=?", dbConnection);
409 removeOld.BindInt64(0, this->trackId);
410 removeOld.Step();
411 }
412
413 {
414 if (replayGain->albumGain != 1.0 || replayGain->albumPeak != 1.0 ||
415 replayGain->albumGain != 1.0 || replayGain->albumPeak != 1.0)
416 {
417 db::Statement insert(
418 "INSERT INTO replay_gain "
419 "(track_id, album_gain, album_peak, track_gain, track_peak) "
420 "VALUES (?, ?, ?, ?, ?);",
421 dbConnection);
422
423 insert.BindInt64(0, this->trackId);
424 insert.BindFloat(1, replayGain->albumGain);
425 insert.BindFloat(2, replayGain->albumPeak);
426 insert.BindFloat(3, replayGain->trackGain);
427 insert.BindFloat(4, replayGain->trackPeak);
428
429 insert.Step();
430 }
431 }
432 }
433 }
434
SaveThumbnail(db::Connection & connection,const std::string & libraryDirectory)435 int64_t IndexerTrack::SaveThumbnail(db::Connection& connection, const std::string& libraryDirectory) {
436 int64_t thumbnailId = 0;
437
438 if (this->internalMetadata->thumbnailData) {
439 const int64_t sum = Checksum(this->internalMetadata->thumbnailData, this->internalMetadata->thumbnailSize);
440
441 db::Statement thumbs("SELECT id FROM thumbnails WHERE filesize=? AND checksum=?", connection);
442 thumbs.BindInt32(0, this->internalMetadata->thumbnailSize);
443 thumbs.BindInt64(1, sum);
444
445 if (thumbs.Step() == db::Row) {
446 thumbnailId = thumbs.ColumnInt64(0); /* thumbnail already exists */
447 }
448
449 if (thumbnailId == 0) { /* doesn't exist yet, let's insert the record and write the file */
450 db::Statement insertThumb("INSERT INTO thumbnails (filesize,checksum) VALUES (?,?)", connection);
451 insertThumb.BindInt32(0, this->internalMetadata->thumbnailSize);
452 insertThumb.BindInt64(1, sum);
453
454 if (insertThumb.Step() == db::Done) {
455 thumbnailId = connection.LastInsertedId();
456
457 std::string filename =
458 libraryDirectory +
459 "thumbs/" +
460 std::to_string(thumbnailId) +
461 ".jpg";
462
463 #ifdef WIN32
464 std::wstring wfilename = u8to16(filename);
465 FILE *thumbFile = _wfopen(wfilename.c_str(), L"wb");
466 #else
467 FILE *thumbFile = fopen(filename.c_str(), "wb");
468 #endif
469 fwrite(this->internalMetadata->thumbnailData, sizeof(char), this->internalMetadata->thumbnailSize, thumbFile);
470 fclose(thumbFile);
471 }
472 }
473 }
474
475 return thumbnailId;
476 }
477
ProcessNonStandardMetadata(db::Connection & connection)478 void IndexerTrack::ProcessNonStandardMetadata(db::Connection& connection) {
479 MetadataMap unknownFields(this->internalMetadata->metadata);
480 removeKnownFields(unknownFields);
481
482 std::map<int64_t, std::set<int64_t>> processed;
483
484 db::Statement selectMetaKey("SELECT id FROM meta_keys WHERE name=?", connection);
485 db::Statement selectMetaValue("SELECT id FROM meta_values WHERE meta_key_id=? AND content=?", connection);
486 db::Statement insertMetaValue("INSERT INTO meta_values (meta_key_id,content) VALUES (?,?)", connection);
487 db::Statement insertTrackMeta("INSERT INTO track_meta (track_id,meta_value_id) VALUES (?,?)", connection);
488 db::Statement insertMetaKey("INSERT INTO meta_keys (name) VALUES (?)", connection);
489
490 MetadataMap::const_iterator it = unknownFields.begin();
491 for ( ; it != unknownFields.end(); ++it){
492 int64_t keyId = 0;
493 std::string key;
494 bool keyCached = false, valueCached = false;
495
496 /* lookup the ID for the key; insert if it doesn't exist.. */
497 if (metadataIdCache.find("metaKey-" + it->first) != metadataIdCache.end()) {
498 keyId = metadataIdCache["metaKey-" + it->first];
499 keyCached = true;
500 }
501 else {
502 selectMetaKey.Reset();
503 selectMetaKey.BindText(0, it->first);
504
505 if (selectMetaKey.Step() == db::Row) {
506 keyId = selectMetaKey.ColumnInt64(0);
507 }
508 else {
509 insertMetaKey.Reset();
510 insertMetaKey.BindText(0, it->first);
511
512 if (insertMetaKey.Step() == db::Done) {
513 keyId = connection.LastInsertedId();
514 }
515 }
516
517 if (keyId != 0) {
518 metadataIdCache["metaKey-" + it->first] = keyId;
519 }
520 }
521
522 if (keyId == 0) {
523 continue; /* welp... */
524 }
525
526 /* see if we already have the value as a normalized row in our table.
527 if we don't, insert it. */
528
529 int64_t valueId = 0;
530
531 if (metadataIdCache.find("metaValue-" + it->second) != metadataIdCache.end()) {
532 valueId = metadataIdCache["metaValue-" + it->second];
533 valueCached = true;
534 }
535 else {
536 selectMetaValue.Reset();
537 selectMetaValue.BindInt64(0, keyId);
538 selectMetaValue.BindText(1, it->second);
539
540 if (selectMetaValue.Step() == db::Row) {
541 valueId = selectMetaValue.ColumnInt64(0);
542 }
543 else {
544 insertMetaValue.Reset();
545 insertMetaValue.BindInt64(0, keyId);
546 insertMetaValue.BindText(1, it->second);
547
548 if (insertMetaValue.Step() == db::Done) {
549 valueId = connection.LastInsertedId();
550 }
551 }
552
553 if (valueId != 0) {
554 metadataIdCache["metaValue-" + it->second] = valueId;
555 }
556 }
557
558 /* now that we have a keyId and a valueId, create the relationship */
559
560 if (valueId != 0 && keyId != 0) {
561
562 /* we allow multiple values for the same key (for example, multiple composers
563 for a track. but we don't allow duplicates. keep track of what keys and
564 values we've already attached to this track, and dont add dupes. */
565
566 bool process = true;
567 if (processed.find(valueId) != processed.end()) {
568 auto keys = processed[valueId];
569 if (keys.find(keyId) != keys.end()) {
570 process = false;
571 }
572 else {
573 keys.insert(keyId);
574 }
575 }
576 else {
577 processed[valueId] = { keyId };
578 }
579
580 if (process) {
581 insertTrackMeta.Reset();
582 insertTrackMeta.BindInt64(0, this->trackId);
583 insertTrackMeta.BindInt64(1, valueId);
584 insertTrackMeta.Step();
585 }
586 }
587 }
588 }
589
createTrackExternalId(IndexerTrack & track)590 static std::string createTrackExternalId(IndexerTrack& track) {
591 size_t hash1 = hash32(track.GetString("filename").c_str());
592
593 size_t hash2 = hash32(
594 (track.GetString("title") +
595 track.GetString("album") +
596 track.GetString("artist") +
597 track.GetString("album_artist") +
598 track.GetString("filesize") +
599 track.GetString("duration")).c_str());
600
601 return std::string("local-") + std::to_string(hash1) + "-" + std::to_string(hash2);
602 }
603
SaveAlbum(db::Connection & dbConnection,int64_t thumbnailId)604 int64_t IndexerTrack::SaveAlbum(db::Connection& dbConnection, int64_t thumbnailId) {
605 std::string album = this->GetString("album");
606 std::string value = album + "-" + this->GetString("album_artist");
607
608 /* ideally we'd use std::hash<>, but on some platforms this returns a 64-bit
609 unsigned number, which cannot be easily used with sqlite3. TODO: this seems
610 really strange, why don't we just cast to a signed int and be done with it?
611 something to do with negative values? i can't remember now. */
612 size_t albumId = hash32(value.c_str());
613
614 std::string cacheKey = "album-" + value;
615 if (metadataIdCache.find(cacheKey) != metadataIdCache.end()) {
616 return metadataIdCache[cacheKey];
617 }
618 else {
619 std::string insertStatement = "INSERT INTO albums (id, name) VALUES (?, ?)";
620 db::Statement insertValue(insertStatement.c_str(), dbConnection);
621 insertValue.BindInt64(0, albumId);
622 insertValue.BindText(1, album);
623
624 if (insertValue.Step() == db::Done) {
625 metadataIdCache[cacheKey] = albumId;
626 }
627 }
628
629 if (thumbnailId != 0) {
630 db::Statement updateStatement(
631 "UPDATE albums SET thumbnail_id=? WHERE id=?", dbConnection);
632
633 updateStatement.BindInt64(0, thumbnailId);
634 updateStatement.BindInt64(1, albumId);
635 updateStatement.Step();
636
637 thumbnailIdCache[(int) albumId] = thumbnailId;
638 }
639
640 return albumId;
641 }
642
SaveSingleValueField(db::Connection & dbConnection,const std::string & trackMetadataKeyName,const std::string & fieldTableName)643 int64_t IndexerTrack::SaveSingleValueField(
644 db::Connection& dbConnection,
645 const std::string& trackMetadataKeyName,
646 const std::string& fieldTableName)
647 {
648 int64_t id = 0;
649
650 std::string selectQuery = u8fmt(
651 "SELECT id FROM %s WHERE name=?", fieldTableName.c_str());
652
653 db::Statement stmt(selectQuery.c_str(), dbConnection);
654 std::string value = this->GetString(trackMetadataKeyName.c_str());
655
656 if (metadataIdCache.find(fieldTableName + "-" + value) != metadataIdCache.end()) {
657 id = metadataIdCache[fieldTableName + "-" + value];
658 }
659 else {
660 stmt.BindText(0, value);
661 if (stmt.Step() == db::Row) {
662 id = stmt.ColumnInt64(0);
663 }
664 else {
665 std::string insertStatement = u8fmt(
666 "INSERT INTO %s (name) VALUES (?)", fieldTableName.c_str());
667
668 db::Statement insertValue(insertStatement.c_str(), dbConnection);
669 insertValue.BindText(0, value);
670
671 if (insertValue.Step() == db::Done) {
672 id = dbConnection.LastInsertedId();
673 }
674 }
675
676 metadataIdCache[fieldTableName + "-" + value] = id;
677 }
678
679 return id;
680 }
681
SaveMultiValueField(db::Connection & connection,const std::string & tracksTableColumnName,const std::string & fieldTableName,const std::string & junctionTableName,const std::string & junctionTableForeignKeyColumnName)682 int64_t IndexerTrack::SaveMultiValueField(
683 db::Connection& connection,
684 const std::string& tracksTableColumnName,
685 const std::string& fieldTableName,
686 const std::string& junctionTableName,
687 const std::string& junctionTableForeignKeyColumnName)
688 {
689 std::string aggregatedValue;
690 int64_t fieldId = 0;
691 int count = 0;
692
693 std::set<std::string> processed; /* for deduping */
694
695 MetadataIteratorRange values = this->GetValues(tracksTableColumnName.c_str());
696 while (values.first != values.second) {
697 if (processed.find(values.first->second) == processed.end()) {
698 processed.insert(values.first->second);
699
700 std::string value = values.first->second;
701
702 fieldId = SaveNormalizedFieldValue(
703 connection,
704 fieldTableName,
705 value,
706 false,
707 junctionTableName,
708 junctionTableForeignKeyColumnName);
709
710 if (count != 0) {
711 aggregatedValue += ", ";
712 }
713
714 aggregatedValue += value;
715
716 ++count;
717 }
718
719 ++values.first;
720 }
721
722 if (count > 1 || fieldId == 0) {
723 fieldId = SaveNormalizedFieldValue(
724 connection, fieldTableName, aggregatedValue, true);
725 }
726
727 return fieldId;
728 }
729
SaveGenre(db::Connection & dbConnection)730 int64_t IndexerTrack::SaveGenre(db::Connection& dbConnection) {
731 return this->SaveMultiValueField(
732 dbConnection,
733 GENRE_TRACK_COLUMN_NAME,
734 GENRES_TABLE_NAME,
735 GENRE_TRACK_JUNCTION_TABLE_NAME,
736 GENRE_TRACK_FOREIGN_KEY);
737 }
738
SaveArtist(db::Connection & dbConnection)739 int64_t IndexerTrack::SaveArtist(db::Connection& dbConnection) {
740 return this->SaveMultiValueField(
741 dbConnection,
742 ARTIST_TRACK_COLUMN_NAME,
743 ARTISTS_TABLE_NAME,
744 ARTIST_TRACK_JUNCTION_TABLE_NAME,
745 ARTIST_TRACK_FOREIGN_KEY);
746 }
747
SaveDirectory(db::Connection & db,const std::string & filename)748 void IndexerTrack::SaveDirectory(db::Connection& db, const std::string& filename) {
749 try {
750 std::string dir = NormalizeDir(
751 boost::filesystem::path(filename).parent_path().string());
752
753 int64_t dirId = -1;
754 if (metadataIdCache.find("directoryId-" + dir) != metadataIdCache.end()) {
755 dirId = metadataIdCache["directoryId-" + dir];
756 }
757 else {
758 db::Statement find("SELECT id FROM directories WHERE name=?", db);
759 find.BindText(0, dir.c_str());
760 if (find.Step() == db::Row) {
761 dirId = find.ColumnInt64(0);
762 }
763 else {
764 db::Statement insert("INSERT INTO directories (name) VALUES (?)", db);
765 insert.BindText(0, dir);
766 if (insert.Step() == db::Done) {
767 dirId = db.LastInsertedId();
768 }
769 }
770
771 if (dirId != -1) {
772 db::Statement update("UPDATE tracks SET directory_id=? WHERE id=?", db);
773 update.BindInt64(0, dirId);
774 update.BindInt64(1, this->trackId);
775 update.Step();
776 }
777 }
778
779 }
780 catch (...) {
781 /* not much we can do, but we don't want the app to die if we're
782 unable to parse its directory. */
783 }
784 }
785
Save(db::Connection & dbConnection,std::string libraryDirectory)786 bool IndexerTrack::Save(db::Connection &dbConnection, std::string libraryDirectory) {
787 static bool disableAlbumArtistFallback =
788 Preferences::ForComponent("settings")
789 ->GetBool(prefs::keys::DisableAlbumArtistFallback, false);
790
791 std::unique_lock<std::mutex> lock(sharedWriteMutex);
792
793 if (!disableAlbumArtistFallback && this->GetString("album_artist") == "") {
794 this->SetValue("album_artist", this->GetString("artist").c_str());
795 }
796
797 if (this->GetString("external_id") == "") {
798 this->SetValue("external_id", createTrackExternalId(*this).c_str());
799 }
800
801 /* remove existing relations -- we're going to update them with fresh data */
802
803 if (this->trackId != 0) {
804 removeRelation(dbConnection, "track_genres", this->trackId);
805 removeRelation(dbConnection, "track_artists", this->trackId);
806 removeRelation(dbConnection, "track_meta", this->trackId);
807 }
808
809 /* write generic info to the tracks table */
810
811 this->trackId = writeToTracksTable(dbConnection, *this);
812
813 if (!this->trackId) {
814 return false;
815 }
816
817 /* see if the metadata reader plugin extracted a thumbnail. if not, we call
818 this->GetThumbnailId() to see if one already exists for the album */
819 int64_t thumbnailId = this->SaveThumbnail(dbConnection, libraryDirectory);
820 if (thumbnailId == 0) {
821 thumbnailId = this->GetThumbnailId();
822 }
823
824 const int64_t albumId = this->SaveAlbum(dbConnection, thumbnailId);
825 const int64_t genreId = this->SaveGenre(dbConnection);
826 const int64_t artistId = this->SaveArtist(dbConnection);
827 int64_t albumArtistId = this->SaveSingleValueField(dbConnection, "album_artist", "artists");
828
829 /* ensure we have a correct source id */
830 int sourceId = 0;
831
832 try {
833 std::string source = this->GetString("source_id");
834 if (source.size()) {
835 sourceId = std::stoi(source.c_str());
836 }
837 }
838 catch (...) {
839 /* shouldn't happen... */
840 }
841
842 /* update all of the track foreign keys */
843
844 {
845 db::Statement stmt(
846 "UPDATE tracks " \
847 "SET album_id=?, visual_genre_id=?, visual_artist_id=?, album_artist_id=?, thumbnail_id=?, source_id=? " \
848 "WHERE id=?", dbConnection);
849
850 stmt.BindInt64(0, albumId);
851 stmt.BindInt64(1, genreId);
852 stmt.BindInt64(2, artistId);
853 stmt.BindInt64(3, albumArtistId);
854 stmt.BindInt64(4, thumbnailId);
855 stmt.BindInt64(5, sourceId);
856 stmt.BindInt64(6, this->trackId);
857 stmt.Step();
858 }
859
860 ProcessNonStandardMetadata(dbConnection);
861
862 /* sometimes indexer source plugins save the 'filename' field with a custom,
863 encoded URI. in these cases the plugin can populate a 'directory' field
864 with the actual directory, if one exists. otherwise, we'll just extract
865 the directory from the 'filename'. */
866 std::string path = this->GetString("directory").size() ?
867 this->GetString("directory") : this->GetString("filename");
868
869 SaveDirectory(dbConnection, path);
870
871 SaveReplayGain(dbConnection);
872
873 return true;
874 }
875
SaveNormalizedFieldValue(db::Connection & dbConnection,const std::string & tableName,const std::string & fieldValue,bool isAggregatedValue,const std::string & relationJunctionTableName,const std::string & relationJunctionTableColumn)876 int64_t IndexerTrack::SaveNormalizedFieldValue(
877 db::Connection &dbConnection,
878 const std::string& tableName,
879 const std::string& fieldValue,
880 bool isAggregatedValue,
881 const std::string& relationJunctionTableName,
882 const std::string& relationJunctionTableColumn)
883 {
884 int64_t fieldId = 0;
885
886 /* find by value */
887
888 {
889 if (metadataIdCache.find(tableName + "-" + fieldValue) != metadataIdCache.end()) {
890 fieldId = metadataIdCache[tableName + "-" + fieldValue];
891 }
892 else {
893 std::string query = u8fmt("SELECT id FROM %s WHERE name=?", tableName.c_str());
894 db::Statement stmt(query.c_str(), dbConnection);
895 stmt.BindText(0, fieldValue);
896
897 if (stmt.Step() == db::Row) {
898 fieldId = stmt.ColumnInt64(0);
899 metadataIdCache[tableName + "-" + fieldValue] = fieldId;
900 }
901 }
902 }
903
904 /* not found? insert. */
905
906 if (fieldId == 0) {
907 std::string query = u8fmt(
908 "INSERT INTO %s (name, aggregated) VALUES (?, ?)", tableName.c_str());
909
910 db::Statement stmt(query.c_str(), dbConnection);
911 stmt.BindText(0, fieldValue);
912 stmt.BindInt32(1, isAggregatedValue ? 1 : 0);
913
914 if (stmt.Step() == db::Done) {
915 fieldId = dbConnection.LastInsertedId();
916 }
917 }
918
919 /* if this is a normalized value it may need to be inserted into a
920 junction table. see if we were asked to do this... */
921
922 if (relationJunctionTableName.size() && relationJunctionTableColumn.size()) {
923 std::string query = u8fmt(
924 "INSERT INTO %s (track_id, %s) VALUES (?, ?)",
925 relationJunctionTableName.c_str(), relationJunctionTableColumn.c_str());
926
927 db::Statement stmt(query.c_str(), dbConnection);
928 stmt.BindInt64(0, this->trackId);
929 stmt.BindInt64(1, fieldId);
930 stmt.Step();
931 }
932
933 return fieldId;
934 }
935
Copy()936 TrackPtr IndexerTrack::Copy() {
937 return TrackPtr(new IndexerTrack(this->trackId));
938 }
939
InternalMetadata()940 IndexerTrack::InternalMetadata::InternalMetadata()
941 : thumbnailData(nullptr)
942 , thumbnailSize(0) {
943 }
944
~InternalMetadata()945 IndexerTrack::InternalMetadata::~InternalMetadata() {
946 delete[] this->thumbnailData;
947 }
948