1 /* DatabaseTracks.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * module() file is part of sayonara player
6  *
7  * module() program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * module() program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with module() program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "Database/Tracks.h"
22 #include "Database/Library.h"
23 #include "Database/Query.h"
24 
25 #include "Utils/MetaData/MetaDataList.h"
26 #include "Utils/MetaData/Genre.h"
27 
28 #include "Utils/Algorithm.h"
29 #include "Utils/Utils.h"
30 #include "Utils/FileUtils.h"
31 #include "Utils/Set.h"
32 #include "Utils/Library/Filter.h"
33 #include "Utils/Library/SearchMode.h"
34 #include "Utils/Logger/Logger.h"
35 
36 #include <QDateTime>
37 #include <QMap>
38 
39 #include <utility>
40 #include <tuple>
41 
42 using DB::Tracks;
43 using DB::Query;
44 using SMM=::Library::SearchModeMask;
45 namespace LibraryUtils=::Library::Utils;
46 using ::Library::Filter;
47 
48 static QString getFilterClause(const Filter& filter, QString cisPlaceholder);
49 
dropTrackView(DB::Module * module,LibraryId libraryId,const QString & trackView)50 static void dropTrackView(DB::Module* module, LibraryId libraryId, const QString& trackView)
51 {
52 	if(libraryId < 0){
53 		return;
54 	}
55 
56 	module->runQuery("DROP VIEW IF EXISTS " + trackView + ";", "Cannot drop " + trackView);
57 }
58 
dropSearchView(DB::Module * module,const QString & trackSearchView)59 static void dropSearchView(DB::Module* module, const QString& trackSearchView)
60 {
61 	module->runQuery("DROP VIEW IF EXISTS " + trackSearchView + "; ", "Cannot drop " + trackSearchView);
62 }
63 
createTrackView(DB::Module * module,LibraryId libraryId,const QString & trackView,const QString & selectStatement)64 static void createTrackView(DB::Module* module, LibraryId libraryId, const QString& trackView, const QString& selectStatement)
65 {
66 	if(libraryId < 0){
67 		return;
68 	}
69 
70 	QString query =	"CREATE VIEW "
71 					+ trackView + " "
72 					"AS " + selectStatement + " "
73 					"FROM tracks "
74 					"WHERE tracks.libraryID = " + QString::number(libraryId);
75 
76 	module->runQuery(query, "Cannot create track view");
77 }
78 
createTrackSearchView(DB::Module * module,LibraryId libraryId,const QString & trackSearchView,const QString & selectStatement)79 static void createTrackSearchView(DB::Module* module, LibraryId libraryId, const QString& trackSearchView, const QString& selectStatement)
80 {
81 	QString query =
82 			"CREATE VIEW "
83 			+ trackSearchView + " "
84 			"AS "
85 			+ selectStatement + ", "
86 			"albums.name			AS albumName, "					// 18
87 			"albums.rating			AS albumRating, "				// 19
88 			"artists.name			AS artistName, "				// 20
89 			"albumArtists.name		AS albumArtistName, "			// 21
90 			"(albums.cissearch || ',' || artists.cissearch || ',' || tracks.cissearch) AS allCissearch, " // 22
91 			"tracks.fileCissearch	AS fileCissearch, "				// 23
92 			"tracks.genreCissearch	AS genreCissearch "				// 24
93 			"FROM tracks "
94 			"LEFT OUTER JOIN albums ON tracks.albumID = albums.albumID "
95 			"LEFT OUTER JOIN artists ON tracks.artistID = artists.artistID "
96 			"LEFT OUTER JOIN artists albumArtists ON tracks.albumArtistID = albumArtists.artistID "
97 	;
98 
99 	if(libraryId >= 0) {
100 		query += "WHERE libraryID=" + QString::number(libraryId);
101 	}
102 
103 	query += ";";
104 
105 	module->runQuery(query, "Cannot create track search view");
106 }
107 
108 Tracks::Tracks() = default;
109 Tracks::~Tracks() = default;
110 
initViews()111 void Tracks::initViews()
112 {
113 	const QStringList fields
114 	{
115 		"tracks.trackID",							// 0
116 		"tracks.title",								// 1
117 		"tracks.length",							// 2
118 		"tracks.year",								// 3
119 		"tracks.bitrate",							// 4
120 		"tracks.filename",							// 5
121 		"tracks.filesize",							// 6
122 		"tracks.track         AS trackNum",			// 7
123 		"tracks.genre",								// 8
124 		"tracks.discnumber",						// 9
125 		"tracks.rating",							// 10
126 		"tracks.albumID	      AS albumID",			// 11
127 		"tracks.artistID      AS artistID",			// 12
128 		"tracks.albumArtistID AS albumArtistID",	// 13
129 		"tracks.comment       AS comment",			// 14
130 		"tracks.createDate",						// 15
131 		"tracks.modifyDate",						// 16
132 		"tracks.libraryID     AS trackLibraryID"	// 17
133 	};
134 
135 	QString select = "SELECT " + fields.join(", ") + " ";
136 
137 	dropTrackView(module(), libraryId(), trackView());
138 	createTrackView(module(), libraryId(), trackView(), select);
139 
140 	dropSearchView(module(), trackSearchView());
141 	createTrackSearchView(module(), libraryId(), trackSearchView(), select);
142 }
143 
fetchQueryTracks() const144 QString Tracks::fetchQueryTracks() const
145 {
146 	return "SELECT * FROM " + trackSearchView() + " ";
147 }
148 
dbFetchTracks(Query & q,MetaDataList & result) const149 bool Tracks::dbFetchTracks(Query& q, MetaDataList& result) const
150 {
151 	result.clear();
152 
153 	if (!q.exec()) {
154 		q.showError("Cannot fetch tracks from database");
155 		return false;
156 	}
157 
158 	while(q.next())
159 	{
160 		MetaData data;
161 
162 		data.setId(				q.value(0).toInt());
163 		data.setTitle(			q.value(1).toString());
164 		data.setDurationMs(		q.value(2).toInt());
165 		data.setYear(			q.value(3).value<Year>());
166 		data.setBitrate(		q.value(4).value<Bitrate>());
167 		data.setFilepath(		q.value(5).toString());
168 		data.setFilesize(		q.value(6).value<Filesize>());
169 		data.setTrackNumber(	q.value(7).value<TrackNum>());
170 		data.setGenres(			q.value(8).toString().split(","));
171 		data.setDiscnumber(		q.value(9).value<Disc>());
172 		data.setRating(			q.value(10).value<Rating>());
173 		data.setAlbumId(		q.value(11).toInt());
174 		data.setArtistId(		q.value(12).toInt());
175 		data.setComment(		q.value(14).toString());
176 		data.setCreatedDate(	q.value(15).value<uint64_t>());
177 		data.setModifiedDate(	q.value(16).value<uint64_t>());
178 		data.setLibraryid(		q.value(17).value<LibraryId>());
179 		data.setAlbum(			q.value(18).toString().trimmed());
180 		data.setArtist(			q.value(20).toString().trimmed());
181 		data.setAlbumArtist(	q.value(21).toString(), q.value(13).toInt());
182 
183 		data.setDatabaseId(module()->databaseId());
184 
185 		result.push_back(std::move(data));
186 	}
187 
188 	return true;
189 }
190 
getMultipleTracksByPath(const QStringList & paths,MetaDataList & tracks) const191 bool Tracks::getMultipleTracksByPath(const QStringList& paths, MetaDataList& tracks) const
192 {
193 	for(const QString& path : paths) {
194 		tracks << getTrackByPath(path);
195 	}
196 
197 	return (tracks.count() == paths.size());
198 }
199 
getTrackByPath(const QString & path) const200 MetaData Tracks::getTrackByPath(const QString& path) const
201 {
202 	const QString cleanedPath = Util::File::cleanFilename(path);
203 	const QString query = fetchQueryTracks() + "WHERE filename = :filename;";
204 
205 	DB::Query q(module());
206 	q.prepare(query);
207 	q.bindValue(":filename", Util::convertNotNull(cleanedPath));
208 
209 	MetaData md(cleanedPath);
210 	md.setDatabaseId(module()->databaseId());
211 
212 	MetaDataList tracks;
213 	if(!dbFetchTracks(q, tracks)) {
214 		return md;
215 	}
216 
217 	if(tracks.empty())
218 	{
219 		md.setExtern(true);
220 		return md;
221 	}
222 
223 	return tracks.first();
224 }
225 
getTrackById(TrackID id) const226 MetaData Tracks::getTrackById(TrackID id) const
227 {
228 	Query q(module());
229 	QString query = fetchQueryTracks() +
230 		" WHERE trackID = :trackId; ";
231 
232 	q.prepare(query);
233 	q.bindValue(":trackId", id);
234 
235 	MetaDataList tracks;
236 	if(!dbFetchTracks(q, tracks)) {
237 		return MetaData();
238 	}
239 
240 	if(tracks.isEmpty()) {
241 		MetaData md;
242 		md.setExtern(true);
243 		return md;
244 	}
245 
246 	return tracks.first();
247 }
248 
getNumTracks() const249 int Tracks::getNumTracks() const
250 {
251 	DB::Query q = module()->runQuery(
252 		"SELECT COUNT(tracks.trackid) FROM tracks WHERE libraryID=:libraryID;",
253 		{":libraryID", libraryId()},
254 		"Cannot count tracks"
255 	);
256 
257 	if(q.hasError() || !q.next()){
258 		return -1;
259 	}
260 
261 	int ret = q.value(0).toInt();
262 	return ret;
263 }
264 
getTracksByIds(const QList<TrackID> & ids,MetaDataList & tracks) const265 bool Tracks::getTracksByIds(const QList<TrackID>& ids, MetaDataList& tracks) const
266 {
267 	QStringList queries;
268 	for(const TrackID& id : ids)
269 	{
270 		queries << fetchQueryTracks() + QString(" WHERE trackID = :trackId%1").arg(id);
271 	}
272 
273 	QString query = queries.join(" UNION ");
274 	query += ";";
275 
276 	Query q(module());
277 	q.prepare(query);
278 
279 	for(TrackID id : ids)
280 	{
281 		q.bindValue(QString(":trackId%1").arg(id), id);
282 	}
283 
284 	return dbFetchTracks(q, tracks);
285 }
286 
getAllTracks(MetaDataList & result) const287 bool Tracks::getAllTracks(MetaDataList& result) const
288 {
289 	Query q(module());
290 
291 	QString query = fetchQueryTracks() + ";";
292 
293 	q.prepare(query);
294 
295 	return dbFetchTracks(q, result);
296 }
297 
getAllTracksByAlbum(const IdList & albumsIds,MetaDataList & result) const298 bool DB::Tracks::getAllTracksByAlbum(const IdList& albumsIds, MetaDataList& result) const
299 {
300     return getAllTracksByAlbum(albumsIds, result, Filter(), -1);
301 }
302 
getAllTracksByAlbum(const IdList & albumIds,MetaDataList & result,const Filter & filter,int discnumber) const303 bool Tracks::getAllTracksByAlbum(const IdList& albumIds, MetaDataList& result, const Filter& filter, int discnumber) const
304 {
305 	if(albumIds.isEmpty()) {
306 		return false;
307 	}
308 
309 	const QStringList searchFilters = filter.searchModeFiltertext(true);
310 	for(const QString& searchFilter : searchFilters)
311 	{
312 		QString query = fetchQueryTracks();
313 		query += " WHERE ";
314 		if( !filter.cleared() )
315 		{
316 			query += getFilterClause(filter, ":cissearch") + " AND ";
317 		}
318 
319 		{ // album id clauses
320 			const QString aidf = trackSearchView() + ".albumID ";
321 
322 			QStringList orClauses;
323 			Util::Algorithm::transform(albumIds, orClauses, [aidf](AlbumId albumId){
324 				return QString("%1 = :albumId%2").arg(aidf).arg(albumId);
325 			});
326 
327 			query += " (" + orClauses.join(" OR ") + ") ";
328 		}
329 
330 		query += ";";
331 
332 		{ // prepare & run
333 			Query q(module());
334 			q.prepare(query);
335 
336 			for(AlbumId albumId : albumIds) {
337 				q.bindValue(QString(":albumId%1").arg(albumId), albumId);
338 			}
339 
340 			q.bindValue(":cissearch", searchFilter);
341 
342 			MetaDataList tmpList;
343 			dbFetchTracks(q, tmpList);
344 
345 			if(discnumber >= 0)
346 			{
347 				tmpList.removeTracks([discnumber](const MetaData& md)
348 				{
349 					return (md.discnumber() != discnumber);
350 				});
351 			}
352 
353 			result.appendUnique(tmpList);
354 		}
355 	}
356 
357 	return true;
358 }
359 
360 
getAllTracksByAlbumArtist(const IdList & artistIds,MetaDataList & result) const361 bool Tracks::getAllTracksByAlbumArtist(const IdList& artistIds, MetaDataList& result) const
362 {
363 	return getAllTracksByAlbumArtist(artistIds, result, Filter());
364 }
365 
getAllTracksByAlbumArtist(const IdList & artistIds,MetaDataList & result,const Filter & filter) const366 bool Tracks::getAllTracksByAlbumArtist(const IdList& artistIds, MetaDataList& result, const Filter& filter) const
367 {
368 	if(artistIds.empty()){
369 		return false;
370 	}
371 
372 	const QStringList searchFilters = filter.searchModeFiltertext(true);
373 	for(const QString& searchFilter : searchFilters)
374 	{
375 		QString query = fetchQueryTracks();
376 		query += " WHERE ";
377 
378 		if( !filter.cleared() )
379 		{
380 			query += getFilterClause(filter, ":cissearch") + " AND ";
381 		}
382 
383 		{ // artist conditions
384 			const QString aidf = trackSearchView() + "." + artistIdField();
385 
386 			QStringList orClauses;
387 			Util::Algorithm::transform(artistIds, orClauses, [aidf](ArtistId artistId){
388 				return QString("%1 = :artistId%2").arg(aidf).arg(artistId);
389 			});
390 
391 			query += " (" + orClauses.join(" OR ") + ") ";
392 		}
393 
394 		query += ";";
395 
396 		{ // prepare & run
397 			Query q(module());
398 			q.prepare(query);
399 
400 			for(ArtistId artistId : artistIds) {
401 				q.bindValue(QString(":artistId%1").arg(artistId), artistId);
402 			}
403 
404 			q.bindValue(":cissearch", searchFilter);
405 
406 			MetaDataList tmpList;
407 			dbFetchTracks(q, tmpList);
408 			result.appendUnique(tmpList);
409 		}
410 	}
411 
412 	return true;
413 }
414 
415 
getAllTracksByArtist(const IdList & artistIds,MetaDataList & result) const416 bool Tracks::getAllTracksByArtist(const IdList& artistIds, MetaDataList& result) const
417 {
418 	return getAllTracksByArtist(artistIds, result, Filter());
419 }
420 
getAllTracksByArtist(const IdList & artistIds,MetaDataList & result,const Filter & filter) const421 bool Tracks::getAllTracksByArtist(const IdList& artistIds, MetaDataList& result, const Filter& filter) const
422 {
423 	if(artistIds.empty()){
424 		return false;
425 	}
426 
427 	const QStringList searchFilters = filter.searchModeFiltertext(true);
428 	for(const QString& searchFilter : searchFilters)
429 	{
430 		QString query = fetchQueryTracks();
431 		query += " WHERE ";
432 
433 		if( !filter.cleared() )
434 		{
435 			query += getFilterClause(filter, ":cissearch") + " AND ";
436 		}
437 
438 		{ // artist conditions
439 			const QString aidf = trackSearchView() + "." + artistIdField();
440 
441 			QStringList orClauses;
442 			Util::Algorithm::transform(artistIds, orClauses, [aidf](ArtistId artistId){
443 				return QString("%1 = :artistId%2").arg(aidf).arg(artistId);
444 			});
445 
446 			query += " (" + orClauses.join(" OR ") + ") ";
447 		}
448 
449 		query += ";";
450 
451 		{ // prepare & run
452 			Query q(module());
453 			q.prepare(query);
454 
455 			for(ArtistId artistId : artistIds) {
456 				q.bindValue(QString(":artistId%1").arg(artistId), artistId);
457 			}
458 
459 			q.bindValue(":cissearch", searchFilter);
460 
461 			MetaDataList tmpList;
462 			dbFetchTracks(q, tmpList);
463 			result.appendUnique(tmpList);
464 		}
465 	}
466 
467 	return true;
468 }
469 
getAllTracksBySearchString(const Filter & filter,MetaDataList & result) const470 bool Tracks::getAllTracksBySearchString(const Filter& filter, MetaDataList& result) const
471 {
472 	const QStringList searchFilters = filter.searchModeFiltertext(true);
473 	for(const QString& searchFilter : searchFilters)
474 	{
475 		QString query = fetchQueryTracks();
476 		query += " WHERE " + getFilterClause(filter, ":cissearch");
477 		query += ";";
478 
479 		Query q(module());
480 		q.prepare(query);
481 		q.bindValue(":cissearch", searchFilter);
482 
483         {
484             MetaDataList tracks;
485             dbFetchTracks(q, tracks);
486 
487             result.appendUnique(tracks);
488         }
489 	}
490 
491 	return true;
492 }
493 
getAllTracksByPaths(const QStringList & paths,MetaDataList & tracks) const494 bool Tracks::getAllTracksByPaths(const QStringList& paths, MetaDataList& tracks) const
495 {
496 	QStringList queries;
497 	for(int i=0; i<paths.size(); i++)
498 	{
499 		queries << fetchQueryTracks() + " WHERE filename LIKE :" + QString("path%1").arg(i);
500 	}
501 
502 	QString query = queries.join(" UNION ") + ";";
503 	Query q(module());
504 	q.prepare(query);
505 	for(int i=0; i<paths.size(); i++)
506 	{
507 		q.bindValue( QString(":path%1").arg(i), paths[i] + "%");
508 	}
509 
510 	bool success = dbFetchTracks(q, tracks);
511 	return success;
512 }
513 
deleteTrack(TrackID id)514 bool Tracks::deleteTrack(TrackID id)
515 {
516 	Query q = module()->runQuery("DELETE FROM tracks WHERE trackID = :trackID", {":trackID", id}, QString("Cannot delete track %1").arg(id));
517 
518 	return (!q.hasError());
519 }
520 
deleteTracks(const IdList & ids)521 bool Tracks::deleteTracks(const IdList& ids)
522 {
523 	module()->db().transaction();
524 
525 	int fileCount = Util::Algorithm::count(ids, [this](Id id)
526 	{
527 		return deleteTrack(id);
528 	});
529 
530 	bool success = module()->db().commit();
531 
532 	return (success && (fileCount == ids.size()));
533 }
534 
deleteTracks(const MetaDataList & tracks)535 bool Tracks::deleteTracks(const MetaDataList& tracks)
536 {
537 	if(tracks.isEmpty()){
538 		return true;
539 	}
540 
541 	module()->db().transaction();
542 
543 	auto deletedTracks = Util::Algorithm::count(tracks, [this](const MetaData& md)
544 	{
545 		return deleteTrack(md.id());
546 	});
547 
548 	module()->db().commit();
549 
550 	spLog(Log::Info, this) << "Deleted " << deletedTracks << " of " << tracks.size() << " tracks";
551 
552 	return (deletedTracks == tracks.count());
553 }
554 
deleteInvalidTracks(const QString & libraryPath,MetaDataList & doubleMetadata)555 bool Tracks::deleteInvalidTracks(const QString& libraryPath, MetaDataList& doubleMetadata)
556 {
557 	doubleMetadata.clear();
558 
559 	MetaDataList tracks;
560 	if(!getAllTracks(tracks))
561 	{
562 		spLog(Log::Error, this) << "Cannot get tracks from db";
563 		return false;
564 	}
565 
566 	QMap<QString, int> map;
567 	IdList toDelete;
568 	int idx = 0;
569 
570 	for(const MetaData& md : tracks)
571 	{
572 		if(map.contains(md.filepath()))
573 		{
574 			spLog(Log::Warning, this) << "found double path: " << md.filepath();
575 			int oldIndex = map[md.filepath()];
576 			toDelete << md.id();
577 			doubleMetadata << tracks[oldIndex];
578 		}
579 
580 		else {
581 			map.insert(md.filepath(), idx);
582 		}
583 
584 		if( (!libraryPath.isEmpty()) &&
585 			(!md.filepath().contains(libraryPath)) )
586 		{
587 			toDelete << md.id();
588 		}
589 
590 		idx++;
591 	}
592 
593 	bool success;
594 	spLog(Log::Debug, this) << "Will delete " << toDelete.size() << " double-tracks";
595 	success = deleteTracks(toDelete);
596 	spLog(Log::Debug, this) << "delete tracks: " << success;
597 
598 	success = deleteTracks(doubleMetadata);
599 	spLog(Log::Debug, this) << "delete other tracks: " << success;
600 
601 	return false;
602 }
603 
getAllGenres() const604 Util::Set<Genre> Tracks::getAllGenres() const
605 {
606 	Query q = module()->runQuery("SELECT genre FROM " + trackView() + " GROUP BY genre;", "Cannot fetch genres");
607 
608 	if(q.hasError()){
609 		return Util::Set<Genre>();
610 	}
611 
612 	Util::Set<Genre> genres;
613 	while(q.next())
614 	{
615 		QString genre = q.value(0).toString();
616 		QStringList subgenres = genre.split(",");
617 
618 		for(const QString& g : subgenres){
619 			genres.insert( Genre(g) );
620 		}
621 	}
622 
623 	spLog(Log::Debug, this) << "Load all genres finished";
624 	return genres;
625 }
626 
updateTrackCissearch()627 void Tracks::updateTrackCissearch()
628 {
629 	MetaDataList tracks;
630 	getAllTracks(tracks);
631 
632 	module()->db().transaction();
633 
634 	for(const MetaData& md : tracks)
635 	{
636 		const QString cis = LibraryUtils::convertSearchstring(md.title());
637 		const QString cisFilepaths = LibraryUtils::convertSearchstring(md.filepath());
638 		const QString cisGenres = LibraryUtils::convertSearchstring(md.genresToString());
639 
640 		module()->update("tracks",
641 		{
642 			{"cissearch", cis},
643 			{"fileCissearch", cisFilepaths},
644 			{"genreCissearch", cisGenres}
645 		},
646 		{"trackId", md.id()},
647 		"Cannot update album cissearch"
648 		);
649 	}
650 
651 	module()->db().commit();
652 }
653 
deleteAllTracks(bool also_views)654 void Tracks::deleteAllTracks(bool also_views)
655 {
656 	if(libraryId() >= 0)
657 	{
658 		if(also_views)
659 		{
660 			dropTrackView(module(), libraryId(), trackView());
661 			dropSearchView(module(), trackSearchView());
662 		}
663 
664 		module()->runQuery
665 		(
666 			"DELETE FROM tracks WHERE libraryId=:libraryId;",
667 			{":libraryId", libraryId()},
668 			"Cannot delete library tracks"
669 		);
670 	}
671 }
672 
updateTrack(const MetaData & md)673 bool Tracks::updateTrack(const MetaData& md)
674 {
675 	if(md.id() < 0 || md.albumId() < 0 || md.artistId() < 0 || md.libraryId() < 0)
676 	{
677 		spLog(Log::Warning, this) << "Cannot update track (value negative): "
678 								   << " ArtistID: " << md.artistId()
679 								   << " AlbumID: " << md.albumId()
680 								   << " TrackID: " << md.id()
681 								   << " LibraryID: " << md.libraryId();
682 		return false;
683 	}
684 
685 	const QString cissearch = LibraryUtils::convertSearchstring(md.title());
686 	const QString fileCissearch = LibraryUtils::convertSearchstring(md.filepath());
687 	const QString genreCissearch = LibraryUtils::convertSearchstring(md.genresToString());
688 
689 	const QString filepath = Util::File::cleanFilename(md.filepath());
690 
691 	QMap<QString, QVariant> bindings
692 	{
693 		{"albumArtistID",	md.albumArtistId()},
694 		{"albumID",			md.albumId()},
695 		{"artistID",		md.artistId()},
696 		{"bitrate",			md.bitrate()},
697 		{"cissearch",		cissearch},
698 		{"discnumber",		md.discnumber()},
699 		{"filecissearch",	fileCissearch},
700 		{"filename",		filepath},
701 		{"filesize",		QVariant::fromValue(md.filesize())},
702 		{"genre",			Util::convertNotNull(md.genresToString())},
703 		{"genreCissearch",	genreCissearch},
704 		{"length",			QVariant::fromValue(md.durationMs())},
705 		{"libraryID",		md.libraryId()},
706 		{"modifydate",		QVariant::fromValue(Util::currentDateToInt())},
707 		{"rating",			QVariant(int(md.rating()))},
708 		{"title",			Util::convertNotNull(md.title())},
709 		{"track",			md.trackNumber()},
710 		{"year",			md.year()},
711 		{"comment",			Util::convertNotNull(md.comment())}
712 	};
713 
714 	Query q = module()->update("tracks", bindings, {"trackId", md.id()}, QString("Cannot update track %1").arg(md.filepath()));
715 
716 	return (!q.hasError());
717 }
718 
updateTracks(const MetaDataList & tracks)719 bool Tracks::updateTracks(const MetaDataList& tracks)
720 {
721 	module()->db().transaction();
722 
723 	int fileCount = Util::Algorithm::count(tracks, [=](const MetaData& md){
724 		return updateTrack(md);
725 	});
726 
727 	bool success = module()->db().commit();
728 
729 	return success && (fileCount == tracks.count());
730 }
731 
732 
renameFilepaths(const QMap<QString,QString> & paths,LibraryId targetLibrary)733 bool Tracks::renameFilepaths(const QMap<QString, QString>& paths, LibraryId targetLibrary)
734 {
735 	module()->db().transaction();
736 
737 	const QStringList originalPaths(paths.keys());
738 	for(const QString& originalPath : originalPaths)
739 	{
740 		MetaDataList tracks;
741 		getAllTracksByPaths({originalPath}, tracks);
742 
743 		const QString newPath = paths[originalPath];
744 		for(const MetaData& md : tracks)
745 		{
746 			QString oldFilepath = md.filepath();
747 			QString newFilepath(oldFilepath);
748 
749 			newFilepath.replace(originalPath, newPath);
750 			renameFilepath(oldFilepath, newFilepath, targetLibrary);
751 		}
752 	}
753 
754 	return module()->db().commit();
755 }
756 
renameFilepath(const QString & oldPath,const QString & newPath,LibraryId targetLibrary)757 bool Tracks::renameFilepath(const QString& oldPath, const QString& newPath, LibraryId targetLibrary)
758 {
759 	Query q = module()->update
760 	(
761 		"Tracks",
762 		{
763 			{"filename", newPath},
764 			{"libraryID", targetLibrary}
765 		},
766 
767 		{"filename", oldPath},
768 		"Could not rename Filepath"
769 	);
770 
771 	return (!q.hasError());
772 }
773 
insertTrackIntoDatabase(const MetaData & md,ArtistId artistId,AlbumId albumId)774 bool Tracks::insertTrackIntoDatabase(const MetaData& md, ArtistId artistId, AlbumId albumId)
775 {
776 	return insertTrackIntoDatabase(md, artistId, albumId, artistId);
777 }
778 
insertTrackIntoDatabase(const MetaData & md,ArtistId artistId,AlbumId albumId,ArtistId albumArtistId)779 bool Tracks::insertTrackIntoDatabase(const MetaData& md, ArtistId artistId, AlbumId albumId, ArtistId albumArtistId)
780 {
781 	if(albumArtistId == -1)
782 	{
783 		albumArtistId = artistId;
784 	}
785 
786 	uint64_t currentTime = Util::currentDateToInt();
787 	uint64_t modifiedTime = md.modifiedDate();
788 	uint64_t createdTime = md.createdDate();
789 
790 	if(!md.createdDateTime().isValid()){
791 		createdTime = currentTime;
792 	}
793 
794 	if(!md.modifiedDateTime().isValid()){
795 		modifiedTime = currentTime;
796 	}
797 
798 	const QString cissearch = ::Library::Utils::convertSearchstring(md.title());
799 	const QString fileCissearch = ::Library::Utils::convertSearchstring(md.filepath());
800 	const QString genreCissearch = ::Library::Utils::convertSearchstring(md.genresToString());
801 	const QString filepath = Util::File::cleanFilename(md.filepath());
802 
803 	QMap<QString, QVariant> bindings =
804 	{
805 		{"filename",		filepath},
806 		{"albumID",			albumId},
807 		{"artistID",		artistId},
808 		{"albumArtistID",	albumArtistId},
809 		{"title",			Util::convertNotNull(md.title())},
810 		{"year",			md.year()},
811 		{"length",			QVariant::fromValue(md.durationMs())},
812 		{"track",			md.trackNumber()},
813 		{"bitrate",			md.bitrate()},
814 		{"genre",			Util::convertNotNull(md.genresToString())},
815 		{"filesize",		QVariant::fromValue(md.filesize())},
816 		{"discnumber",		md.discnumber()},
817 		{"rating",			QVariant(int(md.rating()))},
818 		{"comment",			Util::convertNotNull(md.comment())},
819 		{"cissearch",		cissearch},
820 		{"fileCissearch",	fileCissearch},
821 		{"genreCissearch",	genreCissearch},
822 		{"createdate",		QVariant::fromValue(createdTime)},
823 		{"modifydate",		QVariant::fromValue(modifiedTime)},
824 		{"libraryID",		md.libraryId()}
825 	};
826 
827 	Query q = module()->insert("tracks", bindings, QString("Cannot insert track %1").arg(md.filepath()));
828 
829 	return (!q.hasError());
830 }
831 
832 
getFilterClause(const Filter & filter,QString cisPlaceholder)833 static QString getFilterClause(const Filter& filter, QString cisPlaceholder)
834 {
835 	cisPlaceholder.remove(":");
836 
837 	switch( filter.mode() )
838 	{
839 		case Filter::Genre:
840 			if(filter.isInvalidGenre())
841 			{
842 				return "genre = ''";
843 			}
844 
845 			return "genreCissearch LIKE :" + cisPlaceholder;
846 
847 		case Filter::Filename:
848 			return "fileCissearch LIKE :" + cisPlaceholder;
849 
850 		case Filter::Fulltext:
851 		case Filter::Invalid:
852 		default:
853 			return "allCissearch LIKE :" + cisPlaceholder;
854 	}
855 }
856