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