1 /* PlaylistItemModel.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This 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  * This 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 this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 
22 /*
23 * PlaylistItemModel.cpp
24  *
25  *  Created on: Apr 8, 2011
26  *      Author: Michael Lugmair (Lucio Carreras)
27  */
28 
29 #include "PlaylistModel.h"
30 #include "Components/Playlist/Playlist.h"
31 #include "Components/Playlist/ExternTracksPlaylistGenerator.h"
32 #include "Components/Tagging/UserTaggingOperations.h"
33 #include "Components/Covers/CoverLocation.h"
34 
35 #include "Interfaces/PlaylistInterface.h"
36 
37 #include "Database/Connector.h"
38 #include "Database/CoverConnector.h"
39 
40 #include "Gui/Utils/MimeData/CustomMimeData.h"
41 #include "Gui/Utils/Icons.h"
42 
43 #include "Utils/Utils.h"
44 #include "Utils/Algorithm.h"
45 #include "Utils/FileUtils.h"
46 #include "Utils/Set.h"
47 #include "Utils/globals.h"
48 
49 #include "Utils/Language/Language.h"
50 #include "Utils/Library/SearchMode.h"
51 #include "Utils/MetaData/MetaDataList.h"
52 #include "Utils/Settings/Settings.h"
53 
54 #include <QFont>
55 #include <QUrl>
56 #include <QHash>
57 #include <QPixmap>
58 
59 namespace Algorithm = Util::Algorithm;
60 using Playlist::Model;
61 
62 namespace
63 {
64 	constexpr const auto AlbumSearchPrefix = '%';
65 	constexpr const auto ArtistSearchPrefix = '$';
66 	constexpr const auto JumpPrefix = ':';
67 
68 	static QString convertEntryLook(const QString& entryLook, const MetaData& md)
69 	{
70 		auto ret = entryLook;
71 		ret.replace(QStringLiteral("*"), QChar(Model::Bold));
72 		ret.replace(QStringLiteral("'"), QChar(Model::Italic));
73 
74 		ret.replace(QStringLiteral("%title%"), md.title());
75 		ret.replace(QStringLiteral("%nr%"), QString::number(md.trackNumber()));
76 		ret.replace(QStringLiteral("%artist%"), md.artist());
77 		ret.replace(QStringLiteral("%album%"), md.album());
78 
79 		return ret;
80 	}
81 }
82 
83 enum class PlaylistSearchMode
84 {
85 		Artist,
86 		Album,
87 		Title,
88 		Jump
89 };
90 
91 struct Model::Private
92 {
93 	QHash<AlbumId, QPixmap> coverLookupMap;
94 	int oldRowCount;
95 	int dragIndex;
96 	int rowHeight;
97 	PlaylistPtr playlist;
98 	Tagging::UserOperations* uto = nullptr;
99 	PlaylistCreator* playlistCreator;
100 
101 	Private(PlaylistCreator* playlistCreator, PlaylistPtr playlistArg) :
102 		oldRowCount(0),
103 		dragIndex(-1),
104 		rowHeight(20),
105 		playlist(playlistArg),
106 		playlistCreator {playlistCreator} {}
107 };
108 
109 Model::Model(PlaylistCreator* playlistCreator, PlaylistPtr playlist, QObject* parent) :
110 	SearchableTableModel(parent)
111 {
112 	m = Pimpl::make<Private>(playlistCreator, playlist);
113 
114 	connect(m->playlist.get(), &Playlist::Playlist::sigItemsChanged, this, &Model::playlistChanged);
115 	connect(m->playlist.get(), &Playlist::Playlist::sigTrackChanged, this, &Model::currentTrackChanged);
116 
117 	ListenSettingNoCall(Set::PL_EntryLook, Model::lookChanged);
118 
119 	playlistChanged(0);
120 }
121 
122 Model::~Model() = default;
123 
124 int Model::rowCount([[maybe_unused]] const QModelIndex& parent) const
125 {
126 	return m->playlist->count();
127 }
128 
129 int Model::columnCount([[maybe_unused]] const QModelIndex& parent) const
130 {
131 	return int(ColumnName::NumColumns);
132 }
133 
134 QVariant Model::data(const QModelIndex& index, int role) const
135 {
136 	const auto row = index.row();
137 	const auto col = index.column();
138 	const auto isCurrentTrack = (row == m->playlist->currentTrackIndex());
139 
140 	if(!Util::between(row, m->playlist->count()))
141 	{
142 		return QVariant();
143 	}
144 
145 	if(role == Qt::DisplayRole)
146 	{
147 		if(col == ColumnName::TrackNumber)
148 		{
149 			return (isCurrentTrack)
150 			       ? QString()
151 			       : QString("%1.").arg(row + 1);
152 		}
153 
154 		else if(col == ColumnName::Time)
155 		{
156 			auto durationMs = m->playlist->track(row).durationMs();
157 			return (durationMs / 1000 <= 0)
158 			       ? QVariant()
159 			       : Util::msToString(durationMs, QStringLiteral("$M:$S"));
160 		}
161 
162 		return QVariant();
163 	}
164 
165 	else if(role == Qt::TextAlignmentRole)
166 	{
167 		if(col != ColumnName::Description)
168 		{
169 			return QVariant(Qt::AlignRight | Qt::AlignVCenter);
170 		}
171 	}
172 
173 	else if(role == Qt::DecorationRole)
174 	{
175 		if(col == ColumnName::Cover)
176 		{
177 			const auto& track = m->playlist->track(row);
178 
179 			if(!m->coverLookupMap.contains(track.albumId()))
180 			{
181 				const auto height = m->rowHeight - 6;
182 
183 				const auto coverLocation = Cover::Location::coverLocation(track);
184 				auto* coverDatabase = DB::Connector::instance()->coverConnector();
185 
186 				QPixmap cover;
187 				const auto hash = coverLocation.hash();
188 				coverDatabase->getCover(hash, cover);
189 
190 				if(cover.isNull())
191 				{
192 					cover = QPixmap(coverLocation.preferredPath());
193 				}
194 
195 				if(!cover.isNull())
196 				{
197 					m->coverLookupMap[track.albumId()] =
198 						cover.scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
199 				}
200 			}
201 
202 			return m->coverLookupMap[track.albumId()];
203 		}
204 	}
205 
206 	else if(role == Qt::SizeHintRole)
207 	{
208 		if(col == ColumnName::Cover)
209 		{
210 			const auto h = m->rowHeight - 4;
211 			return QSize(h, h);
212 		}
213 	}
214 
215 	else if(role == Model::EntryLookRole)
216 	{
217 		if(col == ColumnName::Description)
218 		{
219 			return convertEntryLook(GetSetting(Set::PL_EntryLook), m->playlist->track(row));
220 		}
221 	}
222 
223 	else if(role == Model::DragIndexRole)
224 	{
225 		return (row == m->dragIndex);
226 	}
227 
228 	else if(role == Model::RatingRole || role == Qt::EditRole)
229 	{
230 		if(col == ColumnName::Description)
231 		{
232 			const auto& track = m->playlist->track(row);
233 			return (track.radioMode() == RadioMode::Off)
234 			       ? QVariant::fromValue(metadata(row).rating())
235 			       : QVariant::fromValue(Rating::Last);
236 		}
237 	}
238 
239 	else if(role == Model::CurrentPlayingRole)
240 	{
241 		return isCurrentTrack;
242 	}
243 
244 	return QVariant();
245 }
246 
247 bool Model::setData(const QModelIndex& index, const QVariant& value, int role)
248 {
249 	const auto validEdit = (role == Qt::EditRole) && index.isValid();
250 	if(validEdit)
251 	{
252 		const auto row = index.row();
253 		changeRating({row}, value.value<Rating>());
254 	}
255 
256 	return validEdit;
257 }
258 
259 Qt::ItemFlags Model::flags(const QModelIndex& index) const
260 {
261 	if(!index.isValid())
262 	{
263 		return (Qt::ItemIsEnabled | Qt::ItemIsDropEnabled);
264 	}
265 
266 	const auto row = index.row();
267 	if(!Util::between(row, m->playlist->count()) || metadata(row).isDisabled())
268 	{
269 		return Qt::NoItemFlags;
270 	}
271 
272 	const auto itemFlags = QAbstractTableModel::flags(index);
273 	return (index.column() == static_cast<int>(ColumnName::Description))
274 	       ? (itemFlags | Qt::ItemIsEditable)
275 	       : (itemFlags | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
276 }
277 
278 void Model::clear()
279 {
280 	m->playlist->clear();
281 }
282 
283 void Model::removeTracks(const IndexSet& indexes)
284 {
285 	m->playlist->removeTracks(indexes);
286 }
287 
288 IndexSet Model::moveTracks(const IndexSet& indexes, int target_index)
289 {
290 	return m->playlist->moveTracks(indexes, target_index);
291 }
292 
293 IndexSet Model::moveTracksUp(const IndexSet& indexes)
294 {
295 	const auto minRow = *(std::min_element(indexes.begin(), indexes.end()));
296 	return (minRow <= 0)
297 	       ? IndexSet()
298 	       : moveTracks(indexes, minRow - 1);
299 }
300 
301 IndexSet Model::moveTracksDown(const IndexSet& indexes)
302 {
303 	auto minMax = std::minmax_element(indexes.begin(), indexes.end());
304 	int minRow = *(minMax.first);
305 	int maxRow = *(minMax.second);
306 
307 	return (maxRow < rowCount() - 1)
308 	       ? moveTracks(indexes, minRow + static_cast<int>(indexes.size()) + 1)
309 	       : IndexSet();
310 
311 }
312 
313 IndexSet Model::copyTracks(const IndexSet& indexes, int target_index)
314 {
315 	return m->playlist->copyTracks(indexes, target_index);
316 }
317 
318 void Model::changeRating(const IndexSet& indexes, Rating rating)
319 {
320 	MetaDataList tracks;
321 	tracks.reserve(indexes.size());
322 
323 	for(auto idx : indexes)
324 	{
325 		auto track = m->playlist->track(idx);
326 		if(rating != track.rating())
327 		{
328 			tracks << track;
329 			track.setRating(rating);
330 
331 			m->playlist->replaceTrack(idx, track);
332 		}
333 
334 		emit dataChanged(index(idx, 0), index(idx, int(ColumnName::Description)));
335 	}
336 
337 	if(tracks.isEmpty())
338 	{
339 		return;
340 	}
341 
342 	if(!m->uto)
343 	{
344 		m->uto = new Tagging::UserOperations(-1, this);
345 	}
346 
347 	m->uto->setTrackRating(tracks, rating);
348 }
349 
350 void Model::insertTracks(const MetaDataList& tracks, int row)
351 {
352 	m->playlist->insertTracks(tracks, row);
353 }
354 
355 void Model::insertTracks(const QStringList& files, int row)
356 {
357 	auto* playlistGenerator = new ExternTracksPlaylistGenerator(m->playlistCreator, m->playlist);
358 	connect(playlistGenerator, &ExternTracksPlaylistGenerator::sigFinished, playlistGenerator, &QObject::deleteLater);
359 	playlistGenerator->insertPaths(files, row);
360 }
361 
362 void Model::reverseTracks()
363 {
364 	m->playlist->reverse();
365 }
366 
367 int Model::currentTrack() const
368 {
369 	return m->playlist->currentTrackIndex();
370 }
371 
372 const MetaData& Model::metadata(int row) const
373 {
374 	return m->playlist->track(row);
375 }
376 
377 MetaDataList Model::metadata(const IndexSet& rows) const
378 {
379 	MetaDataList tracks;
380 	tracks.reserve(rows.size());
381 
382 	Util::Algorithm::transform(rows, tracks, [this](int row) {
383 		return m->playlist->track(row);
384 	});
385 
386 	return tracks;
387 }
388 
389 QModelIndexList Model::searchResults(const QString& substr)
390 {
391 	QModelIndexList ret;
392 	auto pureSearchString = substr;
393 	auto playlistSearchMode = PlaylistSearchMode::Title;
394 
395 	if(pureSearchString.startsWith(ArtistSearchPrefix))
396 	{
397 		playlistSearchMode = PlaylistSearchMode::Artist;
398 		pureSearchString.remove(ArtistSearchPrefix);
399 	}
400 	else if(pureSearchString.startsWith(AlbumSearchPrefix))
401 	{
402 		playlistSearchMode = PlaylistSearchMode::Album;
403 		pureSearchString.remove(AlbumSearchPrefix);
404 	}
405 	else if(pureSearchString.startsWith(JumpPrefix))
406 	{
407 		playlistSearchMode = PlaylistSearchMode::Jump;
408 		pureSearchString.remove(JumpPrefix);
409 	}
410 
411 	pureSearchString = pureSearchString.trimmed();
412 
413 	if(playlistSearchMode == PlaylistSearchMode::Jump)
414 	{
415 		bool ok;
416 		const auto line = pureSearchString.toInt(&ok);
417 
418 		return (ok && line < rowCount())
419 		       ? QModelIndexList {this->index(line, 0)}
420 		       : QModelIndexList {QModelIndex {}};
421 	}
422 
423 	const auto rows = rowCount();
424 	for(int i = 0; i < rows; i++)
425 	{
426 		const auto& track = m->playlist->track(i);
427 		QString str;
428 		switch(playlistSearchMode)
429 		{
430 			case PlaylistSearchMode::Artist:
431 				str = track.artist();
432 				break;
433 			case PlaylistSearchMode::Album:
434 				str = track.album();
435 				break;
436 			default:
437 				str = track.title();
438 				break;
439 		}
440 
441 		str = Library::Utils::convertSearchstring(str, searchMode());
442 		if(str.contains(pureSearchString))
443 		{
444 			return QModelIndexList {this->index(i, 0)};
445 		}
446 	}
447 
448 	return QModelIndexList {};
449 }
450 
451 using ExtraTriggerMap = SearchableModelInterface::ExtraTriggerMap;
452 
453 ExtraTriggerMap Model::getExtraTriggers()
454 {
455 	ExtraTriggerMap map;
456 
457 	map.insert(ArtistSearchPrefix, Lang::get(Lang::Artist));
458 	map.insert(AlbumSearchPrefix, Lang::get(Lang::Album));
459 	map.insert(JumpPrefix, tr("Goto row"));
460 
461 	return map;
462 }
463 
464 QMimeData* Model::mimeData(const QModelIndexList& indexes) const
465 {
466 	if(indexes.isEmpty())
467 	{
468 		return nullptr;
469 	}
470 
471 	Util::Set<int> rowSet;
472 	for(const auto& index : indexes)
473 	{
474 		rowSet << index.row();
475 	}
476 
477 	auto rows = rowSet.toList();
478 	Algorithm::sort(rows, [](int row1, int row2) {
479 		return (row1 < row2);
480 	});
481 
482 	MetaDataList tracks;
483 	tracks.reserve(static_cast<MetaDataList::size_type>(rows.size()));
484 
485 	for(const auto row : Algorithm::AsConst(rows))
486 	{
487 		if(row < m->playlist->count())
488 		{
489 			tracks << m->playlist->track(row);
490 		}
491 	}
492 
493 	if(tracks.empty())
494 	{
495 		return nullptr;
496 	}
497 
498 	auto* mimedata = new Gui::CustomMimeData(this);
499 	mimedata->setMetadata(tracks);
500 	mimedata->setPlaylistSourceIndex(m->playlist->index());
501 
502 	return mimedata;
503 }
504 
505 bool Model::hasLocalMedia(const IndexSet& rows) const
506 {
507 	const auto& tracks = m->playlist->tracks();
508 
509 	return Algorithm::contains(rows, [tracks](const auto row) {
510 		return (!Util::File::isWWW(tracks[row].filepath()));
511 	});
512 }
513 
514 void Model::setDragIndex(int dragIndex)
515 {
516 	if(Util::between(m->dragIndex, rowCount()))
517 	{
518 		emit dataChanged(index(m->dragIndex, 0), index(m->dragIndex, columnCount() - 1));
519 	}
520 
521 	m->dragIndex = dragIndex;
522 
523 	if(Util::between(dragIndex, rowCount()))
524 	{
525 		emit dataChanged(index(dragIndex, 0), index(dragIndex, columnCount() - 1));
526 	}
527 }
528 
529 void Model::setRowHeight(int rowHeight)
530 {
531 	if(m->rowHeight != rowHeight)
532 	{
533 		m->coverLookupMap.clear();
534 		m->rowHeight = rowHeight;
535 	}
536 }
537 
538 void Model::refreshData()
539 {
540 	m->playlist->enableAll();
541 }
542 
543 void Model::lookChanged()
544 {
545 	emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
546 }
547 
548 void Model::playlistChanged([[maybe_unused]] int playlistIndex)
549 {
550 	if(m->oldRowCount > m->playlist->count())
551 	{
552 		beginRemoveRows(QModelIndex(), m->playlist->count(), m->oldRowCount - 1);
553 		endRemoveRows();
554 	}
555 
556 	else if(m->playlist->count() > m->oldRowCount)
557 	{
558 		beginInsertRows(QModelIndex(), m->oldRowCount, m->playlist->count() - 1);
559 		endInsertRows();
560 	}
561 
562 	if(m->playlist->count() == 0)
563 	{
564 		beginResetModel();
565 		endResetModel();
566 	}
567 
568 	m->oldRowCount = m->playlist->count();
569 
570 	emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
571 	emit sigDataReady();
572 }
573 
574 void Model::currentTrackChanged(int oldIndex, int newIndex)
575 {
576 	if(Util::between(oldIndex, m->playlist->count()))
577 	{
578 		emit dataChanged(index(oldIndex, 0), index(oldIndex, columnCount() - 1));
579 	}
580 
581 	if(Util::between(oldIndex, m->playlist->count()))
582 	{
583 		emit dataChanged(index(newIndex, 0), index(newIndex, columnCount() - 1));
584 		emit sigCurrentTrackChanged(newIndex);
585 	}
586 }
587 
588 void Playlist::Model::deleteTracks(const IndexSet& rows)
589 {
590 	m->playlist->deleteTracks(rows);
591 }
592 
593 void Playlist::Model::findTrack(int index)
594 {
595 	m->playlist->findTrack(index);
596 }
597 
598 int Playlist::Model::playlistIndex() const
599 {
600 	return m->playlist->index();
601 }
602 
603 void Playlist::Model::setBusy(bool b)
604 {
605 	m->playlist->setBusy(b);
606 }
607