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