diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -381,6 +381,7 @@ qml/MetaDataDelegate.qml qml/TracksDiscHeader.qml + qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.qml qml/GridBrowserView.qml qml/GridBrowserDelegate.qml diff --git a/src/audiowrapper.h b/src/audiowrapper.h --- a/src/audiowrapper.h +++ b/src/audiowrapper.h @@ -122,6 +122,8 @@ void positionChanged(qint64 position); + void currentPlayingForRadiosChanged(QString title, QString nowPlaying); + void seekableChanged(bool seekable); void playing(); diff --git a/src/audiowrapper_libvlc.cpp b/src/audiowrapper_libvlc.cpp --- a/src/audiowrapper_libvlc.cpp +++ b/src/audiowrapper_libvlc.cpp @@ -233,7 +233,15 @@ void AudioWrapper::setSource(const QUrl &source) { - d->mMedia = libvlc_media_new_path(d->mInstance, QDir::toNativeSeparators(source.toLocalFile()).toUtf8().constData()); + if(source.isLocalFile()){ + qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource reading local resource"; + d->mMedia = libvlc_media_new_path(d->mInstance, QDir::toNativeSeparators(source.toLocalFile()).toUtf8().constData()); + }else{ + qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource reading remote resource"; + const char * charUrl = source.url().toUtf8().constData(); + d->mMedia = libvlc_media_new_location(d->mInstance, charUrl); + } + if (!d->mMedia) { qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource" << "failed creating media" @@ -557,6 +565,13 @@ mParent->playerPositionSignalChanges(mPreviousPosition); } + + if(this->mMedia){ + QString title = QLatin1String(libvlc_media_get_meta(this->mMedia, libvlc_meta_Title)); + QString nowPlaying = QLatin1String(libvlc_media_get_meta(this->mMedia, libvlc_meta_NowPlaying)); + + Q_EMIT mParent->currentPlayingForRadiosChanged(title, nowPlaying); + } } void AudioWrapperPrivate::signalSeekableChange(bool isSeekable) diff --git a/src/databaseinterface.h b/src/databaseinterface.h --- a/src/databaseinterface.h +++ b/src/databaseinterface.h @@ -356,6 +356,8 @@ ListTrackDataType allTracksData(); + ListTrackDataType allRadiosData(); + ListTrackDataType recentlyPlayedTracksData(int count); ListTrackDataType frequentlyPlayedTracksData(int count); @@ -382,8 +384,9 @@ TrackDataType trackDataFromDatabaseId(qulonglong id); - qulonglong trackIdFromTitleAlbumTrackDiscNumber(const QString &title, const QString &artist, const std::optional &album, - std::optional trackNumber, std::optional discNumber); + TrackDataType radioDataFromDatabaseId(qulonglong id); + + qulonglong trackIdFromTitleAlbumTrackDiscNumber(const QString &title, const QString &artist, const std::optional &album, std::optional trackNumber, std::optional discNumber); qulonglong trackIdFromFileName(const QUrl &fileName); @@ -425,6 +428,10 @@ void finishRemovingTracksList(); + void updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion); + + void updateUIAfterRadioDeleted(qulonglong radioId); + public Q_SLOTS: void insertTracksList(const QList &tracks, const QHash &covers); @@ -437,6 +444,10 @@ void clearData(); + void updateRadioInDatabase(const TrackDataType &oneTrack); + + void deleteRadioInDatabase(qulonglong radioId); + private: enum class TrackFileInsertType { @@ -471,6 +482,8 @@ qulonglong internalTrackIdFromFileName(const QUrl &fileName); + qulonglong internalRadioIdFromHttpAddress(const QString &httpAddress); + ListTrackDataType internalTracksFromAuthor(const QString &artistName); QList internalAlbumIdsFromAuthor(const QString &artistName); @@ -514,6 +527,8 @@ TrackDataType buildTrackDataFromDatabaseRecord(const QSqlRecord &trackRecord) const; + TrackDataType buildRadioDataFromDatabaseRecord(const QSqlRecord &trackRecord) const; + void internalRemoveTracksList(const QList &removedTracks); void internalRemoveTracksList(const QHash &removedTracks, qulonglong sourceId); @@ -538,12 +553,16 @@ ListTrackDataType internalAllTracksPartialData(); + ListTrackDataType internalAllRadiosPartialData(); + ListTrackDataType internalRecentlyPlayedTracksData(int count); ListTrackDataType internalFrequentlyPlayedTracksData(int count); TrackDataType internalOneTrackPartialData(qulonglong databaseId); + TrackDataType internalOneRadioPartialData(qulonglong databaseId); + ListGenreDataType internalAllGenresPartialData(); ListArtistDataType internalAllComposersPartialData(); @@ -569,6 +588,8 @@ void upgradeDatabaseV13(); + void upgradeDatabaseV14(); + void checkDatabaseSchema(); void checkAlbumsTableSchema(); diff --git a/src/databaseinterface.cpp b/src/databaseinterface.cpp --- a/src/databaseinterface.cpp +++ b/src/databaseinterface.cpp @@ -49,17 +49,20 @@ mSelectTrackQuery(mTracksDatabase), mSelectAlbumIdFromTitleQuery(mTracksDatabase), mInsertAlbumQuery(mTracksDatabase), mSelectTrackIdFromTitleAlbumIdArtistQuery(mTracksDatabase), mInsertTrackQuery(mTracksDatabase), mSelectTracksFromArtist(mTracksDatabase), - mSelectTrackFromIdQuery(mTracksDatabase), mSelectCountAlbumsForArtistQuery(mTracksDatabase), + mSelectTrackFromIdQuery(mTracksDatabase), mSelectRadioFromIdQuery(mTracksDatabase), + mSelectCountAlbumsForArtistQuery(mTracksDatabase), mSelectTrackIdFromTitleArtistAlbumTrackDiscNumberQuery(mTracksDatabase), mSelectAllAlbumsFromArtistQuery(mTracksDatabase), mSelectAllArtistsQuery(mTracksDatabase), mInsertArtistsQuery(mTracksDatabase), mSelectArtistByNameQuery(mTracksDatabase), mSelectArtistQuery(mTracksDatabase), mUpdateTrackStatistics(mTracksDatabase), mRemoveTrackQuery(mTracksDatabase), mRemoveAlbumQuery(mTracksDatabase), mRemoveArtistQuery(mTracksDatabase), mSelectAllTracksQuery(mTracksDatabase), + mSelectAllRadiosQuery(mTracksDatabase), mInsertTrackMapping(mTracksDatabase), mUpdateTrackFirstPlayStatistics(mTracksDatabase), mInsertMusicSource(mTracksDatabase), mSelectMusicSource(mTracksDatabase), mUpdateTrackPriority(mTracksDatabase), mUpdateTrackFileModifiedTime(mTracksDatabase), mSelectTracksMapping(mTracksDatabase), mSelectTracksMappingPriority(mTracksDatabase), + mSelectRadioIdFromHttpAddress(mTracksDatabase), mUpdateAlbumArtUriFromAlbumIdQuery(mTracksDatabase), mSelectTracksMappingPriorityByTrackId(mTracksDatabase), mSelectAlbumIdsFromArtist(mTracksDatabase), mSelectAllTrackFilesQuery(mTracksDatabase), mRemoveTracksMappingFromSource(mTracksDatabase), mRemoveTracksMapping(mTracksDatabase), @@ -76,15 +79,17 @@ mSelectCountAlbumsForLyricistQuery(mTracksDatabase), mSelectAllGenresQuery(mTracksDatabase), mSelectGenreForArtistQuery(mTracksDatabase), mSelectGenreForAlbumQuery(mTracksDatabase), mUpdateTrackQuery(mTracksDatabase), mUpdateAlbumArtistQuery(mTracksDatabase), + mUpdateRadioQuery(mTracksDatabase), mUpdateAlbumArtistInTracksQuery(mTracksDatabase), mQueryMaximumTrackIdQuery(mTracksDatabase), mQueryMaximumAlbumIdQuery(mTracksDatabase), mQueryMaximumArtistIdQuery(mTracksDatabase), mQueryMaximumLyricistIdQuery(mTracksDatabase), mQueryMaximumComposerIdQuery(mTracksDatabase), mQueryMaximumGenreIdQuery(mTracksDatabase), mSelectAllArtistsWithGenreFilterQuery(mTracksDatabase), mSelectAllAlbumsShortWithGenreArtistFilterQuery(mTracksDatabase), mSelectAllAlbumsShortWithArtistFilterQuery(mTracksDatabase), mSelectAllRecentlyPlayedTracksQuery(mTracksDatabase), mSelectAllFrequentlyPlayedTracksQuery(mTracksDatabase), mClearTracksTable(mTracksDatabase), mClearAlbumsTable(mTracksDatabase), mClearArtistsTable(mTracksDatabase), mClearComposerTable(mTracksDatabase), mClearGenreTable(mTracksDatabase), mClearLyricistTable(mTracksDatabase), - mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase) + mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase), + mInsertRadioQuery(mTracksDatabase), mDeleteRadioQuery(mTracksDatabase) { } @@ -106,6 +111,8 @@ QSqlQuery mSelectTrackFromIdQuery; + QSqlQuery mSelectRadioFromIdQuery; + QSqlQuery mSelectCountAlbumsForArtistQuery; QSqlQuery mSelectTrackIdFromTitleArtistAlbumTrackDiscNumberQuery; @@ -130,6 +137,8 @@ QSqlQuery mSelectAllTracksQuery; + QSqlQuery mSelectAllRadiosQuery; + QSqlQuery mInsertTrackMapping; QSqlQuery mUpdateTrackFirstPlayStatistics; @@ -144,6 +153,8 @@ QSqlQuery mSelectTracksMapping; + QSqlQuery mSelectRadioIdFromHttpAddress; + QSqlQuery mSelectTracksMappingPriority; QSqlQuery mUpdateAlbumArtUriFromAlbumIdQuery; @@ -206,6 +217,8 @@ QSqlQuery mUpdateTrackQuery; + QSqlQuery mUpdateRadioQuery; + QSqlQuery mUpdateAlbumArtistQuery; QSqlQuery mUpdateAlbumArtistInTracksQuery; @@ -248,6 +261,10 @@ QSqlQuery mSelectTrackIdQuery; + QSqlQuery mInsertRadioQuery; + + QSqlQuery mDeleteRadioQuery; + QSet mModifiedTrackIds; QSet mModifiedAlbumIds; @@ -364,6 +381,29 @@ return result; } +DatabaseInterface::ListTrackDataType DatabaseInterface::allRadiosData() +{ + auto result = ListTrackDataType{}; + + if (!d) { + return result; + } + + auto transactionResult = startTransaction(); + if (!transactionResult) { + return result; + } + + result = internalAllRadiosPartialData(); + + transactionResult = finishTransaction(); + if (!transactionResult) { + return result; + } + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::recentlyPlayedTracksData(int count) { auto result = ListTrackDataType{}; @@ -702,8 +742,31 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::radioDataFromDatabaseId(qulonglong id) +{ + auto result = TrackDataType(); + + if (!d) { + return result; + } + + auto transactionResult = startTransaction(); + if (!transactionResult) { + return result; + } + + result = internalOneRadioPartialData(id); + + transactionResult = finishTransaction(); + if (!transactionResult) { + return result; + } + + return result; +} + qulonglong DatabaseInterface::trackIdFromTitleAlbumTrackDiscNumber(const QString &title, const QString &artist, const std::optional &album, - std::optional trackNumber, std::optional discNumber) + std::optional trackNumber, std::optional discNumber) { auto result = qulonglong(0); @@ -1153,6 +1216,7 @@ upgradeDatabaseV11(); upgradeDatabaseV12(); upgradeDatabaseV13(); + upgradeDatabaseV14(); checkDatabaseSchema(); } else if (listTables.contains(QStringLiteral("DatabaseVersionV9"))) { @@ -1165,13 +1229,17 @@ if (!listTables.contains(QStringLiteral("DatabaseVersionV13"))) { upgradeDatabaseV13(); } + if(!listTables.contains(QStringLiteral("DatabaseVersionV14"))){ + upgradeDatabaseV14(); + } checkDatabaseSchema(); } else { createDatabaseV9(); upgradeDatabaseV11(); upgradeDatabaseV12(); upgradeDatabaseV13(); + upgradeDatabaseV14(); } } @@ -2735,6 +2803,107 @@ qCInfo(orgKdeElisaDatabase) << "finished update to v13 of database schema"; } +void DatabaseInterface::upgradeDatabaseV14() +{ + qCInfo(orgKdeElisaDatabase) << "begin update to v14 of database schema"; + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + const auto &result = createSchemaQuery.exec(QStringLiteral("CREATE TABLE `DatabaseVersionV14` (`Version` INTEGER PRIMARY KEY NOT NULL)")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + QSqlQuery disableForeignKeys(d->mTracksDatabase); + + auto result = disableForeignKeys.exec(QStringLiteral(" PRAGMA foreign_keys=OFF")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << disableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << disableForeignKeys.lastError(); + + Q_EMIT databaseError(); + } + } + + d->mTracksDatabase.transaction(); + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + const auto &result = createSchemaQuery.exec(QStringLiteral("CREATE TABLE `Radios` (" + "`ID` INTEGER PRIMARY KEY AUTOINCREMENT, " + "`HttpAddress` VARCHAR(255) NOT NULL, " + "`Priority` INTEGER NOT NULL, " + "`Title` VARCHAR(85) NOT NULL, " + "`Rating` INTEGER NOT NULL DEFAULT 0, " + "`Genre` VARCHAR(55), " + "`Comment` VARCHAR(255), " + "UNIQUE (" + "`HttpAddress`" + "), " + "UNIQUE (" + "`Priority`, `Title`, `HttpAddress`" + ") " + "CONSTRAINT fk_tracks_genre FOREIGN KEY (`Genre`) REFERENCES `Genre`(`Name`))" + )); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + //Find webradios (french): https://doc.ubuntu-fr.org/liste_radio_france + //English: https://www.radio.fr/language/english (to get the link play a radio and look for streamUrl in the html elements page). + const auto &result = createSchemaQuery.exec(QStringLiteral("INSERT INTO `Radios` (`HttpAddress`, `Priority`, `Title`) " + "SELECT 'http://classicrock.stream.ouifm.fr/ouifm3.mp3', 1, 'OuiFM_Classic_Rock' UNION ALL " + "SELECT 'http://rock70s.stream.ouifm.fr/ouifmseventies.mp3', 1, 'OuiFM_70s' UNION ALL " + "SELECT 'http://jazzradio.ice.infomaniak.ch/jazzradio-high.mp3', 2 , 'Jazz_Radio' UNION ALL " + "SELECT 'http://cdn.nrjaudio.fm/audio1/fr/30601/mp3_128.mp3?origine=playerweb', 1, 'Nostalgie' UNION ALL " + "SELECT 'https://scdn.nrjaudio.fm/audio1/fr/30713/aac_64.mp3?origine=playerweb', 1, 'Nostalgie Johnny' UNION ALL " + "SELECT 'http://sc-classrock.1.fm:8200', 1, 'Classic rock replay' UNION ALL " + "SELECT 'http://agnes.torontocast.com:8151/stream', 1, 'Instrumentals Forever' UNION ALL " + "SELECT 'https://stream.laut.fm/jahfari', 1, 'Jahfari'" + )); + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + d->mTracksDatabase.commit(); + + { + QSqlQuery enableForeignKeys(d->mTracksDatabase); + + auto result = enableForeignKeys.exec(QStringLiteral(" PRAGMA foreign_keys=ON")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << enableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << enableForeignKeys.lastError(); + + Q_EMIT databaseError(); + } + } + + qCInfo(orgKdeElisaDatabase) << "finished update to v14 of database schema"; +} + void DatabaseInterface::checkDatabaseSchema() { checkAlbumsTableSchema(); @@ -3413,6 +3582,29 @@ } } + { + auto selectAllRadiosText = QStringLiteral("SELECT " + "radios.`ID`, " + "radios.`Title`, " + "radios.`HttpAddress`, " + "radios.`Rating`, " + "trackGenre.`Name`, " + "radios.`Comment` " + "FROM " + "`Radios` radios " + "LEFT JOIN `Genre` trackGenre ON trackGenre.`Name` = radios.`Genre` " + ""); + + auto result = prepareQuery(d->mSelectAllRadiosQuery, selectAllRadiosText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectAllRadiosQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectAllRadiosQuery.lastError(); + + Q_EMIT databaseError(); + } + } + { auto selectAllTracksText = QStringLiteral("SELECT " "tracks.`ID`, " @@ -4067,6 +4259,31 @@ Q_EMIT databaseError(); } } + + { + auto selectRadioFromIdQueryText = QStringLiteral("SELECT " + "radios.`ID`, " + "radios.`Title`, " + "radios.`HttpAddress`, " + "radios.`Rating`, " + "trackGenre.`Name`, " + "radios.`Comment` " + "FROM " + "`Radios` radios " + "LEFT JOIN `Genre` trackGenre ON trackGenre.`Name` = radios.`Genre` " + "WHERE " + "radios.`ID` = :radioId " + ""); + + auto result = prepareQuery(d->mSelectRadioFromIdQuery, selectRadioFromIdQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioFromIdQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioFromIdQuery.lastError(); + + Q_EMIT databaseError(); + } + } { auto selectCountAlbumsQueryText = QStringLiteral("SELECT count(*) " "FROM `Albums` album " @@ -4444,6 +4661,24 @@ } } + { + auto selectRadioIdFromHttpAddress = QStringLiteral("SELECT " + "`ID` " + "FROM " + "`Radios` " + "WHERE " + "`HttpAddress` = :httpAddress"); + + auto result = prepareQuery(d->mSelectRadioIdFromHttpAddress, selectRadioIdFromHttpAddress); + + if (!result) { + qCInfo(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioIdFromHttpAddress.lastQuery(); + qCInfo(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioIdFromHttpAddress.lastError(); + + Q_EMIT databaseError(); + } + } + { auto selectTracksMappingPriorityQueryText = QStringLiteral("SELECT " "max(tracks.`Priority`) AS Priority " @@ -4665,6 +4900,66 @@ } } + { + auto insertRadioQueryText = QStringLiteral("INSERT INTO `Radios` " + "(" + "`Title`, " + "`httpAddress`, " + "`Comment`, " + "`Rating`, " + "`Priority`) " + "VALUES " + "(" + ":title, " + ":httpAddress, " + ":comment, " + ":trackRating," + "1)"); + + auto result = prepareQuery(d->mInsertRadioQuery, insertRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mInsertRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mInsertRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + auto deleteRadioQueryText = QStringLiteral("DELETE FROM `Radios` " + "WHERE `ID` = :radioId"); + + auto result = prepareQuery(d->mDeleteRadioQuery, deleteRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mDeleteRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mDeleteRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + auto updateRadioQueryText = QStringLiteral("UPDATE `Radios` " + "SET " + "`HttpAddress` = :httpAddress, " + "`Title` = :title, " + "`Comment` = :comment, " + "`Rating` = :trackRating " + "WHERE " + "`ID` = :radioId"); + + auto result = prepareQuery(d->mUpdateRadioQuery, updateRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mUpdateRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mUpdateRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + { auto updateAlbumArtistQueryText = QStringLiteral("UPDATE `Albums` " "SET " @@ -5872,6 +6167,27 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::buildRadioDataFromDatabaseRecord(const QSqlRecord &trackRecord) const +{ + TrackDataType result; + + result[TrackDataType::key_type::DatabaseIdRole] = trackRecord.value(0); + result[TrackDataType::key_type::TitleRole] = trackRecord.value(1); + + result[TrackDataType::key_type::AlbumRole] = QStringLiteral("Radios"); + result[TrackDataType::key_type::ArtistRole] = trackRecord.value(1); + + result[TrackDataType::key_type::ResourceRole] = trackRecord.value(2); + result[TrackDataType::key_type::RatingRole] = trackRecord.value(3); + if (!trackRecord.value(4).isNull()) { + result[TrackDataType::key_type::GenreRole] = trackRecord.value(4); + } + result[TrackDataType::key_type::CommentRole] = trackRecord.value(5); + result[DataType::key_type::ElementTypeRole] = ElisaUtils::Radio; + + return result; +} + void DatabaseInterface::internalRemoveTracksList(const QList &removedTracks) { QSet modifiedAlbums; @@ -6259,6 +6575,67 @@ d->mUpdateTrackQuery.finish(); } +void DatabaseInterface::updateRadioInDatabase(const TrackDataType &oneTrack) +{ + QSqlQuery query = d->mUpdateRadioQuery; + bool isInsertion = false; + + if(oneTrack.databaseId() == -1ull){ + query = d->mInsertRadioQuery; + isInsertion = true; + } + + query.bindValue(QStringLiteral(":httpAddress"), oneTrack.resourceURI()); + query.bindValue(QStringLiteral(":radioId"), oneTrack.databaseId()); + query.bindValue(QStringLiteral(":title"), oneTrack.title()); + query.bindValue(QStringLiteral(":comment"), oneTrack.comment()); + query.bindValue(QStringLiteral(":trackRating"), oneTrack.rating()); + + auto result = execQuery(query); + + if (!result || !query.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastError(); + }else{ + TrackDataType radio(oneTrack); + radio[TrackDataType::key_type::ArtistRole] = radio[TrackDataType::key_type::TitleRole]; + if(radio[TrackDataType::key_type::DatabaseIdRole] == -1){ + radio[TrackDataType::key_type::DatabaseIdRole] = internalRadioIdFromHttpAddress(oneTrack.resourceURI().toString()); + radio[TrackDataType::key_type::AlbumRole] = QStringLiteral("Radios"); + radio[TrackDataType::key_type::ArtistRole] = radio[TrackDataType::key_type::TitleRole]; + radio[DataType::key_type::ElementTypeRole] = ElisaUtils::Radio; + //Genre and rating missing for now, see buildRadioDataFromDatabaseRecord. Should be added if used for radios. + } + Q_EMIT updateUIAfterRadioInsertOrUpdate(radio, isInsertion); + } + + query.finish(); +} + +void DatabaseInterface::deleteRadioInDatabase(qulonglong radioId) +{ + QSqlQuery query = d->mDeleteRadioQuery; + + query.bindValue(QStringLiteral(":radioId"), radioId); + + auto result = execQuery(query); + + if (!result || !query.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastError(); + }else{ + Q_EMIT updateUIAfterRadioDeleted(radioId); + } + + query.finish(); +} + void DatabaseInterface::removeAlbumInDatabase(qulonglong albumId) { d->mRemoveAlbumQuery.bindValue(QStringLiteral(":albumId"), albumId); @@ -6600,6 +6977,42 @@ return result; } +qulonglong DatabaseInterface::internalRadioIdFromHttpAddress(const QString &httpAddress) +{ + auto result = qulonglong(0); + + if (!d) { + return result; + } + + d->mSelectRadioIdFromHttpAddress.bindValue(QStringLiteral(":httpAddress"), httpAddress); + + auto queryResult = execQuery(d->mSelectRadioIdFromHttpAddress); + + if (!queryResult || !d->mSelectRadioIdFromHttpAddress.isSelect() || !d->mSelectRadioIdFromHttpAddress.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.lastError(); + + d->mSelectRadioIdFromHttpAddress.finish(); + + return result; + } + + if (d->mSelectRadioIdFromHttpAddress.next()) { + const auto ¤tRecordValue = d->mSelectRadioIdFromHttpAddress.record().value(0); + if (currentRecordValue.isValid()) { + result = currentRecordValue.toULongLong(); + } + } + + d->mSelectRadioIdFromHttpAddress.finish(); + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::internalTracksFromAuthor(const QString &ArtistName) { auto allTracks = ListTrackDataType{}; @@ -6775,6 +7188,27 @@ return result; } +DatabaseInterface::ListTrackDataType DatabaseInterface::internalAllRadiosPartialData() +{ + auto result = ListTrackDataType{}; + + if (!internalGenericPartialData(d->mSelectAllRadiosQuery)) { + return result; + } + + while(d->mSelectAllRadiosQuery.next()) { + const auto ¤tRecord = d->mSelectAllRadiosQuery.record(); + + auto newData = buildRadioDataFromDatabaseRecord(currentRecord); + + result.push_back(newData); + } + + d->mSelectAllRadiosQuery.finish(); + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::internalRecentlyPlayedTracksData(int count) { auto result = ListTrackDataType{}; @@ -6842,6 +7276,27 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::internalOneRadioPartialData(qulonglong databaseId) +{ + auto result = TrackDataType{}; + + d->mSelectRadioFromIdQuery.bindValue(QStringLiteral(":radioId"), databaseId); + + if (!internalGenericPartialData(d->mSelectRadioFromIdQuery)) { + return result; + } + + if (d->mSelectRadioFromIdQuery.next()) { + const auto ¤tRecord = d->mSelectRadioFromIdQuery.record(); + + result = buildRadioDataFromDatabaseRecord(currentRecord); + } + + d->mSelectRadioFromIdQuery.finish(); + + return result; +} + DatabaseInterface::ListGenreDataType DatabaseInterface::internalAllGenresPartialData() { ListGenreDataType result; diff --git a/src/elisaapplication.cpp b/src/elisaapplication.cpp --- a/src/elisaapplication.cpp +++ b/src/elisaapplication.cpp @@ -361,21 +361,22 @@ QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::playerSourceChanged, d->mAudioWrapper.get(), &AudioWrapper::setSource); QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::startedPlayingTrack, d->mMusicManager->viewDatabase(), &DatabaseInterface::trackHasStartedPlaying); + QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::currentPlayingForRadiosChanged, d->mMediaPlayList.get(), &MediaPlayList::updateRadioData); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::ensurePlay, d->mAudioControl.get(), &ManageAudioPlayer::ensurePlay); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::playListFinished, d->mAudioControl.get(), &ManageAudioPlayer::playListFinished); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::currentTrackChanged, d->mAudioControl.get(), &ManageAudioPlayer::setCurrentTrack); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::clearPlayListPlayer, d->mAudioControl.get(), &ManageAudioPlayer::saveForUndoClearPlaylist); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::undoClearPlayListPlayer, d->mAudioControl.get(), &ManageAudioPlayer::restoreForUndoClearPlaylist); - QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::playbackStateChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerPlaybackState); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::statusChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerStatus); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::errorChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerError); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::durationChanged, d->mAudioControl.get(), &ManageAudioPlayer::setAudioDuration); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::seekableChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerIsSeekable); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::positionChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerPosition); + QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::currentPlayingForRadiosChanged, d->mAudioControl.get(), &ManageAudioPlayer::setCurrentPlayingForRadios); d->mPlayerControl->setPlayListModel(d->mMediaPlayList.get()); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::currentTrackChanged, d->mPlayerControl.get(), &ManageMediaPlayerControl::setCurrentTrack); diff --git a/src/elisaqmlplugin.cpp b/src/elisaqmlplugin.cpp --- a/src/elisaqmlplugin.cpp +++ b/src/elisaqmlplugin.cpp @@ -169,6 +169,7 @@ qRegisterMetaType("ElisaUtils::EntryDataList"); qRegisterMetaType("ElisaUtils::FilterType"); qRegisterMetaType("DatabaseInterface::TrackDataType"); + qRegisterMetaType("TrackDataType"); qRegisterMetaType("DatabaseInterface::AlbumDataType"); qRegisterMetaType("DatabaseInterface::ArtistDataType"); qRegisterMetaType("DatabaseInterface::GenreDataType"); diff --git a/src/elisautils.h b/src/elisautils.h --- a/src/elisautils.h +++ b/src/elisautils.h @@ -55,6 +55,7 @@ Track, FileName, Unknown, + Radio, }; Q_ENUM_NS(PlayListEntryType) diff --git a/src/manageaudioplayer.h b/src/manageaudioplayer.h --- a/src/manageaudioplayer.h +++ b/src/manageaudioplayer.h @@ -203,6 +203,8 @@ void startedPlayingTrack(const QUrl &fileName, const QDateTime &time); + void currentPlayingForRadiosChanged(const QVariant &value, int role); + public Q_SLOTS: void setCurrentTrack(const QPersistentModelIndex ¤tTrack); @@ -237,6 +239,8 @@ void setPlayerPosition(qint64 playerPosition); + void setCurrentPlayingForRadios(const QString title, const QString nowPlaying); + void setPlayControlPosition(int playerPosition); void setPersistentState(const QVariantMap &persistentStateValue); diff --git a/src/manageaudioplayer.cpp b/src/manageaudioplayer.cpp --- a/src/manageaudioplayer.cpp +++ b/src/manageaudioplayer.cpp @@ -413,6 +413,13 @@ QTimer::singleShot(0, [this]() {Q_EMIT playControlPositionChanged();}); } +void ManageAudioPlayer::setCurrentPlayingForRadios(const QString title, const QString nowPlaying){ + if(mPlayListModel && mCurrentTrack.isValid()){ + Q_EMIT currentPlayingForRadiosChanged(title, MediaPlayList::TitleRole); + Q_EMIT currentPlayingForRadiosChanged(nowPlaying, MediaPlayList::ArtistRole); + } +} + void ManageAudioPlayer::setPlayControlPosition(int playerPosition) { Q_EMIT seek(playerPosition); diff --git a/src/mediaplaylist.h b/src/mediaplaylist.h --- a/src/mediaplaylist.h +++ b/src/mediaplaylist.h @@ -268,6 +268,8 @@ void undoClearPlayList(); + void updateRadioData(const QVariant &value, int role); + private Q_SLOTS: void loadPlayListLoaded(); @@ -293,7 +295,7 @@ void enqueueFilesList(const ElisaUtils::EntryDataList &newEntries); - void enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries); + void enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries, ElisaUtils::PlayListEntryType type); void enqueueOneEntry(const ElisaUtils::EntryData &entryData, ElisaUtils::PlayListEntryType type); diff --git a/src/mediaplaylist.cpp b/src/mediaplaylist.cpp --- a/src/mediaplaylist.cpp +++ b/src/mediaplaylist.cpp @@ -220,7 +220,8 @@ return modelModified; } - if (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsPlayingRole) { + if ((role != ColumnsRoles::TitleRole && role != ColumnsRoles::ArtistRole) && + (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsPlayingRole)) { return modelModified; } @@ -241,6 +242,32 @@ break; } + case ColumnsRoles::TitleRole: + { + modelModified = true; + d->mData[index.row()].mTitle = value; + d->mTrackData[index.row()][static_cast(role)] = value; + Q_EMIT dataChanged(index, index, {role}); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; + } + case ColumnsRoles::ArtistRole: + { + modelModified = true; + d->mData[index.row()].mArtist = value; + d->mTrackData[index.row()][static_cast(role)] = value; + Q_EMIT dataChanged(index, index, {role}); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; + } default: modelModified = false; } @@ -425,13 +452,13 @@ Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::IsPlayingRole}); } -void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries) +void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries, ElisaUtils::PlayListEntryType type) { enqueueCommon(); beginInsertRows(QModelIndex(), d->mData.size(), d->mData.size() + newEntries.size() - 1); for (const auto &newTrack : newEntries) { - auto newMediaPlayListEntry = MediaPlayListEntry{std::get<0>(newTrack), std::get<1>(newTrack), ElisaUtils::Track}; + auto newMediaPlayListEntry = MediaPlayListEntry{std::get<0>(newTrack), std::get<1>(newTrack), type}; d->mData.push_back(newMediaPlayListEntry); d->mTrackData.push_back({}); Q_EMIT newEntryInList(newMediaPlayListEntry.mId, newMediaPlayListEntry.mTitle.toString(), newMediaPlayListEntry.mEntryType); @@ -565,6 +592,14 @@ Q_EMIT undoClearPlayListPlayer(); } +void MediaPlayList::updateRadioData(const QVariant &value, int role) +{ + auto convertedRole = static_cast(role); + if(d->mCurrentTrack.data(convertedRole) != value){ + this->setData(d->mCurrentTrack, value, role); + } +} + void MediaPlayList::enqueueCommon() { displayOrHideUndoInline(false); @@ -633,6 +668,7 @@ case ElisaUtils::Artist: case ElisaUtils::Genre: case ElisaUtils::Track: + case ElisaUtils::Radio: enqueueOneEntry(newEntry, databaseIdType); break; case ElisaUtils::FileName: @@ -674,7 +710,8 @@ switch (databaseIdType) { case ElisaUtils::Track: - enqueueTracksListById(newEntries); + case ElisaUtils::Radio: + enqueueTracksListById(newEntries, databaseIdType); break; case ElisaUtils::FileName: enqueueFilesList(newEntries); @@ -921,6 +958,24 @@ resetCurrentTrack(); } continue; + } else if(oneEntry.mEntryType == ElisaUtils::Radio ){ + if (track.databaseId() != oneEntry.mId) { + continue; + } + + d->mTrackData[i] = track; + oneEntry.mId = track.databaseId(); + oneEntry.mIsValid = true; + + Q_EMIT dataChanged(index(i, 0), index(i, 0), {}); + + restorePlayListPosition(); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; } else if (oneEntry.mEntryType != ElisaUtils::Artist && !oneEntry.mIsValid && !oneEntry.mTrackUrl.isValid()) { if (track.find(TrackDataType::key_type::TitleRole) != track.end() && track.title() != oneEntry.mTitle) { diff --git a/src/modeldataloader.h b/src/modeldataloader.h --- a/src/modeldataloader.h +++ b/src/modeldataloader.h @@ -62,8 +62,14 @@ void allTracksData(const ModelDataLoader::ListTrackDataType &allData); + void allRadiosData(const ModelDataLoader::ListTrackDataType &radiosData); + + void allRadiosDataUIUpdate(TrackDataType radiosData, bool isInsertion); + void allTrackData(const ModelDataLoader::TrackDataType &allData); + void allRadioData(const ModelDataLoader::TrackDataType &allData); + void tracksAdded(ModelDataLoader::ListTrackDataType newData); void trackModified(const ModelDataLoader::TrackDataType &modifiedTrack); @@ -82,6 +88,12 @@ void albumModified(const ModelDataLoader::AlbumDataType &modifiedAlbum); + void saveRadioModified(TrackDataType trackDataType); + + void deleteRadioInDatabase(qulonglong radioId); + + void radioRemoved(qulonglong radioId); + public Q_SLOTS: void loadData(ElisaUtils::PlayListEntryType dataType); @@ -107,6 +119,14 @@ void loadFrequentlyPlayedData(ElisaUtils::PlayListEntryType dataType); + void updateRadioData(TrackDataType trackDataType); + + void deleteRadioData(qulonglong radioId); + + void updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion); + + void updateUIAfterRadioDeleted(qulonglong radioId); + private: void databaseTracksAdded(const ListTrackDataType &newData); diff --git a/src/modeldataloader.cpp b/src/modeldataloader.cpp --- a/src/modeldataloader.cpp +++ b/src/modeldataloader.cpp @@ -75,6 +75,14 @@ this, &ModelDataLoader::databaseArtistsAdded); connect(database, &DatabaseInterface::artistRemoved, this, &ModelDataLoader::databaseArtistRemoved); + connect(this, &ModelDataLoader::saveRadioModified, + database, &DatabaseInterface::updateRadioInDatabase); + connect(this, &ModelDataLoader::deleteRadioInDatabase, + database, &DatabaseInterface::deleteRadioInDatabase); + connect(database, &DatabaseInterface::updateUIAfterRadioInsertOrUpdate, + this, &ModelDataLoader::updateUIAfterRadioInsertOrUpdate); + connect(database, &DatabaseInterface::updateUIAfterRadioDeleted, + this, &ModelDataLoader::updateUIAfterRadioDeleted); } void ModelDataLoader::loadData(ElisaUtils::PlayListEntryType dataType) @@ -106,6 +114,9 @@ case ElisaUtils::FileName: case ElisaUtils::Unknown: break; + case ElisaUtils::Radio: + Q_EMIT allRadiosData(d->mDatabase->allRadiosData()); + break; } } @@ -135,6 +146,7 @@ break; case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -160,6 +172,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -185,6 +198,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -211,6 +225,7 @@ case ElisaUtils::Track: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -229,6 +244,9 @@ case ElisaUtils::Track: Q_EMIT allTrackData(d->mDatabase->trackDataFromDatabaseId(databaseId)); break; + case ElisaUtils::Radio: + Q_EMIT allRadioData(d->mDatabase->radioDataFromDatabaseId(databaseId)); + break; case ElisaUtils::Album: case ElisaUtils::Artist: case ElisaUtils::Composer: @@ -263,6 +281,7 @@ case ElisaUtils::Genre: case ElisaUtils::Lyricist: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -287,6 +306,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -311,6 +331,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -439,5 +460,20 @@ Q_EMIT albumModified(modifiedAlbum); } +void ModelDataLoader::updateRadioData(TrackDataType trackDataType){ + Q_EMIT saveRadioModified(trackDataType); +} + +void ModelDataLoader::deleteRadioData(qulonglong radioId){ + Q_EMIT deleteRadioInDatabase(radioId); +} + +void ModelDataLoader::updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion){ + Q_EMIT allRadiosDataUIUpdate(radio, isInsertion); +} + +void ModelDataLoader::updateUIAfterRadioDeleted(qulonglong radioId){ + Q_EMIT radioRemoved(radioId); +} #include "moc_modeldataloader.cpp" diff --git a/src/models/datamodel.h b/src/models/datamodel.h --- a/src/models/datamodel.h +++ b/src/models/datamodel.h @@ -117,10 +117,14 @@ void tracksAdded(DataModel::ListTrackDataType newData); + void radiosAdded(DataModel::ListTrackDataType newData); + void trackModified(const DataModel::TrackDataType &modifiedTrack); void trackRemoved(qulonglong removedTrackId); + void radioRemoved(qulonglong removedRadioId); + void genresAdded(DataModel::ListGenreDataType newData); void artistsAdded(DataModel::ListArtistDataType newData); @@ -137,14 +141,20 @@ ElisaUtils::PlayListEntryType modelType, ElisaUtils::FilterType filter, const QString &genre, const QString &artist, qulonglong databaseId); + void radiosModifiedUI(const DataModel::TrackDataType radiosData, bool isInsertion); + private Q_SLOTS: void cleanedDatabase(); private: + void radioModified(const TrackDataType &modifiedRadio); + int trackIndexFromId(qulonglong id) const; + int radioIndexFromId(qulonglong id) const; + void connectModel(DatabaseInterface *database); void setBusy(bool value); @@ -154,6 +164,8 @@ void askModelData(); + void removeRadios(); + std::unique_ptr d; }; diff --git a/src/models/datamodel.cpp b/src/models/datamodel.cpp --- a/src/models/datamodel.cpp +++ b/src/models/datamodel.cpp @@ -34,6 +34,8 @@ DataModel::ListTrackDataType mAllTrackData; + DataModel::ListTrackDataType mAllRadiosData; + DataModel::ListAlbumDataType mAllAlbumData; DataModel::ListArtistDataType mAllArtistData; @@ -127,11 +129,14 @@ Q_ASSERT(index.isValid()); Q_ASSERT(index.column() == 0); - Q_ASSERT(index.row() >= 0 && index.row() < dataCount); Q_ASSERT(!index.parent().isValid()); Q_ASSERT(index.model() == this); Q_ASSERT(index.internalId() == 0); + if(d->mModelType != ElisaUtils::Radio){ + Q_ASSERT(index.row() >= 0 && index.row() < dataCount); + } + switch(role) { case Qt::DisplayRole: @@ -149,6 +154,9 @@ case ElisaUtils::Genre: result = d->mAllGenreData[index.row()][GenreDataType::key_type::TitleRole]; break; + case ElisaUtils::Radio: + result = d->mAllRadiosData[index.row()][GenreDataType::key_type::TitleRole]; + break; case ElisaUtils::Lyricist: case ElisaUtils::Composer: case ElisaUtils::FileName: @@ -158,13 +166,48 @@ break; case DatabaseInterface::ColumnsRoles::DurationRole: { - if (d->mModelType == ElisaUtils::Track) { + switch (d->mModelType) + { + case ElisaUtils::Track: + { auto trackDuration = d->mAllTrackData[index.row()][TrackDataType::key_type::DurationRole].toTime(); if (trackDuration.hour() == 0) { result = trackDuration.toString(QStringLiteral("mm:ss")); } else { result = trackDuration.toString(); } + break; + } + case ElisaUtils::Radio: + result = QStringLiteral(""); + break; + case ElisaUtils::Album: + case ElisaUtils::Artist: + case ElisaUtils::Genre: + case ElisaUtils::Lyricist: + case ElisaUtils::Composer: + case ElisaUtils::FileName: + case ElisaUtils::Unknown: + break; + } + break; + } + case DatabaseInterface::ColumnsRoles::IsSingleDiscAlbumRole: + { + switch (d->mModelType) + { + case ElisaUtils::Track: + case ElisaUtils::Radio: + result = false; + break; + case ElisaUtils::Album: + case ElisaUtils::Artist: + case ElisaUtils::Genre: + case ElisaUtils::Lyricist: + case ElisaUtils::Composer: + case ElisaUtils::FileName: + case ElisaUtils::Unknown: + break; } break; } @@ -183,6 +226,9 @@ case ElisaUtils::Genre: result = d->mAllGenreData[index.row()][static_cast(role)]; break; + case ElisaUtils::Radio: + result = d->mAllRadiosData[index.row()][static_cast(role)]; + break; case ElisaUtils::Lyricist: case ElisaUtils::Composer: case ElisaUtils::FileName: @@ -358,12 +404,31 @@ return result; } +int DataModel::radioIndexFromId(qulonglong id) const +{ + int result; + + for (result = 0; result < d->mAllRadiosData.size(); ++result) { + if (d->mAllRadiosData[result].databaseId() == id) { + return result; + } + } + + result = -1; + + return result; +} + void DataModel::connectModel(DatabaseInterface *database) { d->mDataLoader.setDatabase(database); connect(&d->mDataLoader, &ModelDataLoader::allTracksData, this, &DataModel::tracksAdded); + connect(&d->mDataLoader, &ModelDataLoader::allRadiosData, + this, &DataModel::radiosAdded); + connect(&d->mDataLoader, &ModelDataLoader::allRadiosDataUIUpdate, + this, &DataModel::radiosModifiedUI); connect(&d->mDataLoader, &ModelDataLoader::allAlbumsData, this, &DataModel::albumsAdded); connect(&d->mDataLoader, &ModelDataLoader::allArtistsData, @@ -388,6 +453,8 @@ this, &DataModel::artistsAdded); connect(&d->mDataLoader, &ModelDataLoader::artistRemoved, this, &DataModel::artistRemoved); + connect(&d->mDataLoader, &ModelDataLoader::radioRemoved, + this, &DataModel::radioRemoved); } void DataModel::tracksAdded(ListTrackDataType newData) @@ -451,6 +518,67 @@ } } +void DataModel::radiosAdded(ListTrackDataType newData) +{ + if (newData.isEmpty() && d->mModelType == ElisaUtils::Radio) { + setBusy(false); + } + + if (newData.isEmpty() || d->mModelType != ElisaUtils::Radio) { + return; + } + + if (d->mFilterType == ElisaUtils::FilterById && !d->mAllRadiosData.isEmpty()) { + for (const auto &newTrack : newData) { + auto trackIndex = trackIndexFromId(newTrack.databaseId()); + + if (trackIndex != -1) { + continue; + } + + bool trackInserted = false; + for (int trackIndex = 0; trackIndex < d->mAllRadiosData.count(); ++trackIndex) { + const auto &oneTrack = d->mAllRadiosData[trackIndex]; + + if (oneTrack.trackNumber() > newTrack.trackNumber()) { + beginInsertRows({}, trackIndex, trackIndex); + d->mAllRadiosData.insert(trackIndex, newTrack); + endInsertRows(); + + if (d->mAllRadiosData.size() == 1) { + setBusy(false); + } + + trackInserted = true; + break; + } + } + + if (!trackInserted) { + beginInsertRows({}, d->mAllRadiosData.count(), d->mAllRadiosData.count()); + d->mAllRadiosData.insert(d->mAllRadiosData.count(), newTrack); + endInsertRows(); + + if (d->mAllRadiosData.size() == 1) { + setBusy(false); + } + } + } + } else { + if (d->mAllRadiosData.isEmpty()) { + beginInsertRows({}, 0, newData.size() - 1); + d->mAllRadiosData.swap(newData); + endInsertRows(); + + setBusy(false); + } else { + beginInsertRows({}, d->mAllRadiosData.size(), d->mAllRadiosData.size() + newData.size() - 1); + d->mAllRadiosData.append(newData); + endInsertRows(); + } + } +} + void DataModel::trackModified(const TrackDataType &modifiedTrack) { if (d->mModelType != ElisaUtils::Track) { @@ -488,6 +616,22 @@ } } +void DataModel::radioModified(const TrackDataType &modifiedRadio) +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + auto trackIndex = radioIndexFromId(modifiedRadio.databaseId()); + + if (trackIndex == -1) { + return; + } + + d->mAllRadiosData[trackIndex] = modifiedRadio; + Q_EMIT dataChanged(index(trackIndex, 0), index(trackIndex, 0)); +} + void DataModel::trackRemoved(qulonglong removedTrackId) { if (d->mModelType != ElisaUtils::Track) { @@ -520,6 +664,52 @@ } } +void DataModel::radioRemoved(qulonglong removedRadioId) +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + + auto itRadio = std::find_if(d->mAllRadiosData.begin(), d->mAllRadiosData.end(), + [removedRadioId](auto track) {return track.databaseId() == removedRadioId;}); + + if (itRadio == d->mAllRadiosData.end()) { + return; + } + + auto position = itRadio - d->mAllRadiosData.begin(); + + beginRemoveRows({}, position, position); + d->mAllRadiosData.erase(itRadio); + endRemoveRows(); +} + +void DataModel::radiosModifiedUI(const DataModel::TrackDataType radioData, bool isInsertion){ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + if(isInsertion){ + ListTrackDataType list; + list.append(radioData); + radiosAdded(list); + }else{ + radioModified(radioData); + } +} + +void DataModel::removeRadios() +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + beginRemoveRows({}, 0, d->mAllRadiosData.size()); + d->mAllRadiosData.clear(); + endRemoveRows(); +} + void DataModel::genresAdded(DataModel::ListGenreDataType newData) { if (newData.isEmpty() && d->mModelType == ElisaUtils::Genre) { diff --git a/src/models/trackmetadatamodel.h b/src/models/trackmetadatamodel.h --- a/src/models/trackmetadatamodel.h +++ b/src/models/trackmetadatamodel.h @@ -53,6 +53,10 @@ READ lyrics NOTIFY lyricsChanged) + Q_PROPERTY(bool isRadio + READ isRadio + WRITE setIsRadio) + public: enum ColumnRoles @@ -95,6 +99,8 @@ QString lyrics() const; + bool isRadio(); + Q_SIGNALS: void needDataByDatabaseId(ElisaUtils::PlayListEntryType dataType, qulonglong databaseId); @@ -109,22 +115,48 @@ void lyricsChanged(); + void saveRadioData(TrackDataType trackDataType); + + void deleteRadioData(qulonglong radioId); + + void disableApplyButton(); + + void hideDeleteButton(); + + void showDeleteButton(); + + void closeWindow(); + public Q_SLOTS: void trackData(const TrackMetadataModel::TrackDataType &trackData); void initializeByTrackId(qulonglong databaseId); void initializeByTrackFileName(const QString &fileName); + void initializeForNewRadio(); + void setManager(MusicListenersManager *newManager); + void setIsRadio(bool isRadio); + void setDatabase(DatabaseInterface *trackDatabase); + void saveData(); + + void deleteRadio(); + + void updateUI(TrackDataType radiosData, bool isInsertion); + + void radioRemoved(qulonglong radioId); + protected: void fillDataFromTrackData(const TrackMetadataModel::TrackDataType &trackData); + void fillDataForNewRadio(); + virtual void filterDataFromTrackData(); void removeMetaData(DatabaseInterface::ColumnsRoles metaData); @@ -144,6 +176,10 @@ void fetchLyrics(); + QVariant dataGeneral(const QModelIndex &index, int role) const; + + QVariant dataRadio(const QModelIndex &index, int role) const; + TrackDataType mFullData; TrackDataType mTrackData; @@ -158,12 +194,13 @@ MusicListenersManager *mManager = nullptr; + bool mIsRadio; + FileScanner mFileScanner; QMimeDatabase mMimeDatabase; QFutureWatcher mLyricsValueWatcher; - }; #endif // TRACKMETADATAMODEL_H diff --git a/src/models/trackmetadatamodel.cpp b/src/models/trackmetadatamodel.cpp --- a/src/models/trackmetadatamodel.cpp +++ b/src/models/trackmetadatamodel.cpp @@ -23,6 +23,16 @@ #include +const QList mFieldsForClassicTrack({DatabaseInterface::TitleRole, DatabaseInterface::ArtistRole, DatabaseInterface::AlbumRole, + DatabaseInterface::AlbumArtistRole, DatabaseInterface::TrackNumberRole, DatabaseInterface::DiscNumberRole, + DatabaseInterface::RatingRole, DatabaseInterface::GenreRole, DatabaseInterface::LyricistRole, + DatabaseInterface::ComposerRole, DatabaseInterface::CommentRole, DatabaseInterface::YearRole, + DatabaseInterface::LastPlayDate, DatabaseInterface::PlayCounter}); + +const QList mFieldsForRadioTrack({DatabaseInterface::TitleRole,DatabaseInterface::ResourceRole, DatabaseInterface::CommentRole, DatabaseInterface::DatabaseIdRole, + DatabaseInterface::ArtistRole, DatabaseInterface::AlbumRole}); + + TrackMetadataModel::TrackMetadataModel(QObject *parent) : QAbstractListModel(parent) { @@ -46,7 +56,7 @@ return mTrackData.count(); } -QVariant TrackMetadataModel::data(const QModelIndex &index, int role) const +QVariant TrackMetadataModel::dataGeneral(const QModelIndex &index, int role) const { auto result = QVariant{}; @@ -162,15 +172,17 @@ case DatabaseInterface::LyricsRole: result = i18nc("Lyrics label for track metadata view", "Lyrics"); break; + case DatabaseInterface::ResourceRole: + result = i18nc("Radio HTTP address for radio metadata view", "Stream Http Address"); + break; case DatabaseInterface::SecondaryTextRole: case DatabaseInterface::ImageUrlRole: case DatabaseInterface::ShadowForImageRole: case DatabaseInterface::ChildModelRole: case DatabaseInterface::StringDurationRole: case DatabaseInterface::MilliSecondsDurationRole: case DatabaseInterface::AllArtistsRole: case DatabaseInterface::HighestTrackRating: - case DatabaseInterface::ResourceRole: case DatabaseInterface::IdRole: case DatabaseInterface::DatabaseIdRole: case DatabaseInterface::IsSingleDiscAlbumRole: @@ -191,6 +203,9 @@ case DatabaseInterface::TitleRole: result = TextEntry; break; + case DatabaseInterface::ResourceRole: + result = TextEntry; + break; case DatabaseInterface::ArtistRole: result = TextEntry; break; @@ -245,7 +260,124 @@ case DatabaseInterface::MilliSecondsDurationRole: case DatabaseInterface::AllArtistsRole: case DatabaseInterface::HighestTrackRating: + case DatabaseInterface::IdRole: + case DatabaseInterface::DatabaseIdRole: + case DatabaseInterface::IsSingleDiscAlbumRole: + case DatabaseInterface::ContainerDataRole: + case DatabaseInterface::IsPartialDataRole: + case DatabaseInterface::AlbumIdRole: + case DatabaseInterface::HasEmbeddedCover: + case DatabaseInterface::FileModificationTime: + case DatabaseInterface::FirstPlayDate: + case DatabaseInterface::PlayFrequency: + case DatabaseInterface::ElementTypeRole: + break; + } + break; + } + + return result; +} + +QVariant TrackMetadataModel::dataRadio(const QModelIndex &index, int role) const +{ + auto result = QVariant{}; + + const auto currentKey = mTrackKeys[index.row()]; + + switch (role) + { + case Qt::DisplayRole: + result = mTrackData[currentKey]; + break; + case ItemNameRole: + switch (currentKey) + { + case DatabaseInterface::TitleRole: + result = i18nc("Track title for track metadata view", "Title"); + break; + case DatabaseInterface::CommentRole: + result = i18nc("Comment label for track metadata view", "Comment"); + break; case DatabaseInterface::ResourceRole: + result = i18nc("Radio HTTP address for radio metadata view", "Stream Http Address"); + break; + case DatabaseInterface::ChannelsRole: + case DatabaseInterface::BitRateRole: + case DatabaseInterface::SampleRateRole: + case DatabaseInterface::LastPlayDate: + case DatabaseInterface::PlayCounter: + case DatabaseInterface::LyricsRole: + case DatabaseInterface::YearRole: + case DatabaseInterface::ComposerRole: + case DatabaseInterface::ArtistRole: + case DatabaseInterface::AlbumRole: + case DatabaseInterface::AlbumArtistRole: + case DatabaseInterface::TrackNumberRole: + case DatabaseInterface::DiscNumberRole: + case DatabaseInterface::RatingRole: + case DatabaseInterface::GenreRole: + case DatabaseInterface::LyricistRole: + case DatabaseInterface::DurationRole: + case DatabaseInterface::SecondaryTextRole: + case DatabaseInterface::ImageUrlRole: + case DatabaseInterface::ShadowForImageRole: + case DatabaseInterface::ChildModelRole: + case DatabaseInterface::StringDurationRole: + case DatabaseInterface::MilliSecondsDurationRole: + case DatabaseInterface::AllArtistsRole: + case DatabaseInterface::HighestTrackRating: + case DatabaseInterface::IdRole: + case DatabaseInterface::DatabaseIdRole: + case DatabaseInterface::IsSingleDiscAlbumRole: + case DatabaseInterface::ContainerDataRole: + case DatabaseInterface::IsPartialDataRole: + case DatabaseInterface::AlbumIdRole: + case DatabaseInterface::HasEmbeddedCover: + case DatabaseInterface::FileModificationTime: + case DatabaseInterface::FirstPlayDate: + case DatabaseInterface::PlayFrequency: + case DatabaseInterface::ElementTypeRole: + break; + } + break; + case ItemTypeRole: + switch (currentKey) + { + case DatabaseInterface::TitleRole: + result = TextEntry; + break; + case DatabaseInterface::ResourceRole: + result = TextEntry; + break; + case DatabaseInterface::CommentRole: + result = TextEntry; + break; + case DatabaseInterface::ArtistRole: + case DatabaseInterface::AlbumRole: + case DatabaseInterface::AlbumArtistRole: + case DatabaseInterface::TrackNumberRole: + case DatabaseInterface::DiscNumberRole: + case DatabaseInterface::RatingRole: + case DatabaseInterface::GenreRole: + case DatabaseInterface::LyricistRole: + case DatabaseInterface::ComposerRole: + case DatabaseInterface::YearRole: + case DatabaseInterface::LastPlayDate: + case DatabaseInterface::PlayCounter: + case DatabaseInterface::LyricsRole: + case DatabaseInterface::DurationRole: + case DatabaseInterface::SampleRateRole: + case DatabaseInterface::BitRateRole: + case DatabaseInterface::ChannelsRole: + case DatabaseInterface::SecondaryTextRole: + case DatabaseInterface::ImageUrlRole: + case DatabaseInterface::ShadowForImageRole: + case DatabaseInterface::ChildModelRole: + case DatabaseInterface::StringDurationRole: + case DatabaseInterface::MilliSecondsDurationRole: + case DatabaseInterface::AllArtistsRole: + case DatabaseInterface::HighestTrackRating: case DatabaseInterface::IdRole: case DatabaseInterface::DatabaseIdRole: case DatabaseInterface::IsSingleDiscAlbumRole: @@ -265,10 +397,20 @@ return result; } +QVariant TrackMetadataModel::data(const QModelIndex &index, int role) const +{ + if(this->mIsRadio){ + return dataRadio(index, role); + }else { + return dataGeneral(index, role); + } +} + bool TrackMetadataModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (data(index, role) != value) { - // FIXME: Implement me! + mTrackData[mTrackKeys[index.row()]] = value; + emit dataChanged(index, index, QVector() << role); return true; } @@ -325,11 +467,7 @@ mTrackData.clear(); mTrackKeys.clear(); - for (auto role : {DatabaseInterface::TitleRole, DatabaseInterface::ArtistRole, DatabaseInterface::AlbumRole, - DatabaseInterface::AlbumArtistRole, DatabaseInterface::TrackNumberRole, DatabaseInterface::DiscNumberRole, - DatabaseInterface::RatingRole, DatabaseInterface::GenreRole, DatabaseInterface::LyricistRole, - DatabaseInterface::ComposerRole, DatabaseInterface::CommentRole, DatabaseInterface::YearRole, - DatabaseInterface::LastPlayDate, DatabaseInterface::PlayCounter}) { + for (DatabaseInterface::ColumnsRoles role : (isRadio() ? mFieldsForRadioTrack : mFieldsForClassicTrack)){ if (trackData.constFind(role) != trackData.constEnd()) { if (role == DatabaseInterface::RatingRole) { if (trackData[role].toInt() == 0) { @@ -417,10 +555,25 @@ &mDataLoader, &ModelDataLoader::loadDataByDatabaseId); connect(this, &TrackMetadataModel::needDataByFileName, &mDataLoader, &ModelDataLoader::loadDataByFileName); - connect(&mDataLoader, &ModelDataLoader::allTrackData, - this, &TrackMetadataModel::trackData); + connect(&mDataLoader, &ModelDataLoader::trackModified, this, &TrackMetadataModel::trackData); + + if(isRadio()){ + connect(this, &TrackMetadataModel::saveRadioData, + &mDataLoader, &ModelDataLoader::updateRadioData); + connect(this, &TrackMetadataModel::deleteRadioData, + &mDataLoader, &ModelDataLoader::deleteRadioData); + connect(&mDataLoader, &ModelDataLoader::allRadiosDataUIUpdate, + this, &TrackMetadataModel::updateUI); + connect(&mDataLoader, &ModelDataLoader::radioRemoved, + this, &TrackMetadataModel::radioRemoved); + connect(&mDataLoader, &ModelDataLoader::allRadioData, + this, &TrackMetadataModel::trackData); + }else{ + connect(&mDataLoader, &ModelDataLoader::allTrackData, + this, &TrackMetadataModel::trackData); + } } void TrackMetadataModel::fetchLyrics() @@ -445,7 +598,41 @@ Q_EMIT lyricsChanged(); - Q_EMIT needDataByDatabaseId(ElisaUtils::Track, databaseId); + Q_EMIT needDataByDatabaseId((isRadio() ? ElisaUtils::Radio : ElisaUtils::Track), databaseId); +} + +void TrackMetadataModel::initializeForNewRadio() +{ + mFullData.clear(); + mTrackData.clear(); + + fillDataForNewRadio(); +} + +void TrackMetadataModel::fillDataForNewRadio() +{ + beginResetModel(); + mTrackData.clear(); + mTrackKeys.clear(); + + for (auto role : { + DatabaseInterface::TitleRole, + DatabaseInterface::ResourceRole, + DatabaseInterface::CommentRole, + DatabaseInterface::DatabaseIdRole + + }) { + mTrackKeys.push_back(role); + if(role == DatabaseInterface::DatabaseIdRole){ + mTrackData[role] = -1; + Q_EMIT hideDeleteButton(); + }else{ + mTrackData[role] = QStringLiteral(""); + } + + } + filterDataFromTrackData(); + endResetModel(); } void TrackMetadataModel::initializeByTrackFileName(const QString &fileName) @@ -465,10 +652,41 @@ initialize(newManager, nullptr); } +void TrackMetadataModel::setIsRadio(bool isRadio){ + this->mIsRadio = isRadio; +} + void TrackMetadataModel::setDatabase(DatabaseInterface *trackDatabase) { initialize(nullptr, trackDatabase); } +bool TrackMetadataModel::isRadio(){ + return this->mIsRadio; +} + +void TrackMetadataModel::saveData() +{ + Q_EMIT saveRadioData(mTrackData); +} + +void TrackMetadataModel::deleteRadio() +{ + if(mTrackData[DatabaseInterface::DatabaseIdRole]>=0){ + Q_EMIT deleteRadioData(mTrackData[DatabaseInterface::DatabaseIdRole].toULongLong()); + } +} + +void TrackMetadataModel::updateUI(TrackDataType radiosData, bool isInsertion){ + if(isInsertion){ + mTrackData[DatabaseInterface::DatabaseIdRole] = radiosData[DatabaseInterface::DatabaseIdRole]; + Q_EMIT showDeleteButton(); + } + Q_EMIT disableApplyButton(); +} + +void TrackMetadataModel::radioRemoved(qulonglong radioId){ + Q_EMIT closeWindow(); +} #include "moc_trackmetadatamodel.cpp" diff --git a/src/models/viewsmodel.cpp b/src/models/viewsmodel.cpp --- a/src/models/viewsmodel.cpp +++ b/src/models/viewsmodel.cpp @@ -39,25 +39,28 @@ mTypes = {ViewManager::Context, ViewManager::RecentlyPlayedTracks, ViewManager::FrequentlyPlayedTracks, ViewManager::AllAlbums, ViewManager::AllArtists, ViewManager::AllTracks, - ViewManager::AllGenres, ViewManager::FilesBrowser}; + ViewManager::AllGenres, ViewManager::FilesBrowser, + ViewManager::RadiosBrowser}; mNames = {{ViewManager::Context, {i18nc("Title of the view of the playlist", "Now Playing")}}, {ViewManager::RecentlyPlayedTracks, {i18nc("Title of the view of recently played tracks", "Recently Played")}}, {ViewManager::FrequentlyPlayedTracks, {i18nc("Title of the view of frequently played tracks", "Frequently Played")}}, {ViewManager::AllAlbums, {i18nc("Title of the view of all albums", "Albums")}}, {ViewManager::AllArtists, {i18nc("Title of the view of all artists", "Artists")}}, {ViewManager::AllTracks, {i18nc("Title of the view of all tracks", "Tracks")}}, {ViewManager::AllGenres, {i18nc("Title of the view of all genres", "Genres")}}, - {ViewManager::FilesBrowser, {i18nc("Title of the file browser view", "Files")}}}; + {ViewManager::FilesBrowser, {i18nc("Title of the file browser view", "Files")}}, + {ViewManager::RadiosBrowser, {i18nc("Title of the file radios browser view", "Radios")}}}; mIcons = {{ViewManager::Context, QUrl{QStringLiteral("image://icon/view-media-lyrics")}}, {ViewManager::RecentlyPlayedTracks, QUrl{QStringLiteral("image://icon/media-playlist-play")}}, {ViewManager::FrequentlyPlayedTracks, QUrl{QStringLiteral("image://icon/amarok_playcount")}}, {ViewManager::AllAlbums, QUrl{QStringLiteral("image://icon/view-media-album-cover")}}, {ViewManager::AllArtists, QUrl{QStringLiteral("image://icon/view-media-artist")}}, {ViewManager::AllTracks, QUrl{QStringLiteral("image://icon/view-media-track")}}, {ViewManager::AllGenres, QUrl{QStringLiteral("image://icon/view-media-genre")}}, - {ViewManager::FilesBrowser, QUrl{QStringLiteral("image://icon/document-open-folder")}}}; + {ViewManager::FilesBrowser, QUrl{QStringLiteral("image://icon/document-open-folder")}}, + {ViewManager::RadiosBrowser, QUrl{QStringLiteral("image://icon/radio")}}}; } }; @@ -167,6 +170,8 @@ return 6; case ViewManager::FilesBrowser: return 7; + case ViewManager::RadiosBrowser: + return 8; case ViewManager::OneAlbum: case ViewManager::OneArtist: case ViewManager::OneAlbumFromArtist: diff --git a/src/qml/ContentView.qml b/src/qml/ContentView.qml --- a/src/qml/ContentView.qml +++ b/src/qml/ContentView.qml @@ -107,6 +107,7 @@ displaySingleAlbum: displaySingleAlbum, showSection: showDiscHeaders, opacity: 0, + radioCase: radioCase }) } @@ -140,7 +141,6 @@ }) } - onPopOneView: { if (browseStackView.depth > 2) { browseStackView.pop() } @@ -377,7 +377,7 @@ } Component { - id: albumContext + id: albumContext ContextView { StackView.onActivated: viewManager.viewIsLoaded(viewType) diff --git a/src/qml/DataListView.qml b/src/qml/DataListView.qml --- a/src/qml/DataListView.qml +++ b/src/qml/DataListView.qml @@ -35,6 +35,12 @@ property alias sortRole: proxyModel.sortRole property var sortAscending property bool displaySingleAlbum: false + property bool radioCase: false + + function openMetaDataView(databaseId){ + metadataLoader.setSource("MediaTrackMetadataView.qml", {"databaseId": databaseId, "radio": viewHeader.radioCase}); + metadataLoader.active = true + } DataModel { id: realModel @@ -48,6 +54,12 @@ onEntriesToEnqueue: elisa.mediaPlayList.enqueue(newEntries, databaseIdType, enqueueMode, triggerPlay) } + Loader { + id: metadataLoader + active: false + onLoaded: item.show() + } + Component { id: singleAlbumDelegate @@ -90,6 +102,10 @@ listView.currentIndex = index } } + + onCallOpenMetaDataView: { + openMetaDataView(databaseId) + } } } @@ -130,6 +146,10 @@ listView.currentIndex = index entry.forceActiveFocus() } + + onCallOpenMetaDataView: { + openMetaDataView(databaseId) + } } } @@ -148,6 +168,8 @@ allowArtistNavigation: isSubPage + showCreateRadioButton: viewHeader.radioCase + onShowArtist: { viewManager.openChildView(secondaryTitle, '', elisaTheme.artistIcon, 0, ElisaUtils.Artist, ViewManager.NoDiscHeaders) } @@ -176,6 +198,14 @@ modelType) } + Connections { + target: listView.navigationBar + + onCreateRadio: { + openMetaDataView(-1) + } + } + Component.onCompleted: { if (elisa.musicManager) { realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, filterType, mainTitle, secondaryTitle, databaseId) diff --git a/src/qml/ListBrowserDelegate.qml b/src/qml/ListBrowserDelegate.qml --- a/src/qml/ListBrowserDelegate.qml +++ b/src/qml/ListBrowserDelegate.qml @@ -44,6 +44,7 @@ signal clicked() signal enqueue(var databaseId, var name) signal replaceAndPlay(var databaseId, var name) + signal callOpenMetaDataView(var databaseId) Accessible.role: Accessible.ListItem Accessible.name: title @@ -61,13 +62,7 @@ text: i18nc("Show track metadata", "View Details") icon.name: "help-about" onTriggered: { - if (metadataLoader.active === false) { - metadataLoader.active = true - } - else { - metadataLoader.item.close(); - metadataLoader.active = false - } + callOpenMetaDataView(databaseId) } } @@ -81,17 +76,6 @@ Keys.onReturnPressed: enqueue(databaseId, title) Keys.onEnterPressed: enqueue(databaseId, title) - Loader { - id: metadataLoader - active: false - onLoaded: item.show() - - sourceComponent: MediaTrackMetadataView { - databaseId: mediaTrack.databaseId - onRejected: metadataLoader.active = false; - } - } - Rectangle { id: rowRoot @@ -219,7 +203,7 @@ id: mainLabelDetailed text: { - if (trackNumber !== 0) { + if (trackNumber > 0) { return i18nc("%1: track number. %2: track title", "%1 - %2", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); } else { @@ -255,7 +239,7 @@ } if (album !== '') { labelText += ' - ' + album - if (!isSingleDiscAlbum) { + if (!isSingleDiscAlbum && discNumber !== -1) { labelText += ' - CD ' + discNumber } } diff --git a/src/qml/ListBrowserView.qml b/src/qml/ListBrowserView.qml --- a/src/qml/ListBrowserView.qml +++ b/src/qml/ListBrowserView.qml @@ -42,6 +42,9 @@ property alias currentIndex: contentDirectoryView.currentIndex property alias enableSorting: navigationBar.enableSorting property var stackView + property bool showEnqueueButton: true + property bool showCreateRadioButton + property alias navigationBar: navigationBar signal goBack() signal showArtist(var name) @@ -67,6 +70,9 @@ Layout.fillWidth: true + showEnqueueButton: listView.showEnqueueButton + showCreateRadioButton: listView.showCreateRadioButton + Binding { target: contentModel property: 'filterText' diff --git a/src/qml/MediaTrackMetadataView.qml b/src/qml/MediaTrackMetadataView.qml --- a/src/qml/MediaTrackMetadataView.qml +++ b/src/qml/MediaTrackMetadataView.qml @@ -29,18 +29,26 @@ property int databaseId: 0 property string fileName + property bool radio: false signal rejected() LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true - title: i18nc("Window title for track metadata", "View Details") + title: { + if(trackMetadata.radio && databaseId === -1){ + return i18nc("Window title for track metadata", "Create a Radio") + } + + return i18nc("Window title for track metadata", "View Details") + } TrackMetadataModel { id: realModel - manager: elisa.musicManager + isRadio: trackMetadata.radio + manager: elisa.musicManager //Important } modality: Qt.NonModal @@ -80,6 +88,8 @@ Layout.minimumWidth: elisaTheme.coverImageSize Layout.maximumHeight: elisaTheme.coverImageSize Layout.maximumWidth: elisaTheme.coverImageSize + + visible: !trackMetadata.radio } ListView { @@ -105,7 +115,9 @@ model: realModel delegate: MetaDataDelegate { + radio: trackMetadata.radio width: scrollBar.visible ? (!LayoutMirroring.enabled ? trackData.width - scrollBar.width : trackData.width) : trackData.width + onRadioEdited: applyButton.enabled = true } } } @@ -136,35 +148,92 @@ elide: Text.ElideRight } + + visible: !trackMetadata.radio + } + + RowLayout{ + spacing: elisaTheme.layoutVerticalMargin + + DialogButtonBox { + Layout.minimumHeight: implicitHeight + alignment: Qt.AlignLeft + + Button { + id: deleteButton + text: qsTr("Delete") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: realModel.deleteRadio() + } + } + + DialogButtonBox { + id: buttons + + Layout.fillWidth: true + Layout.minimumHeight: implicitHeight + alignment: Qt.AlignRight + + Button { + id: applyButton + + text: qsTr("Apply") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + onClicked: realModel.saveData() + enabled: false + } + Button { + text: qsTr("Close") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: trackMetadata.close() + } + } + + visible: trackMetadata.radio } DialogButtonBox { - id: buttons + id: buttonsTrack Layout.fillWidth: true Layout.minimumHeight: implicitHeight standardButtons: DialogButtonBox.Close alignment: Qt.AlignRight - onRejected: trackMetadata.rejected() + onRejected: trackMetadata.close() + + visible: !trackMetadata.radio } } Connections { target: elisa onMusicManagerChanged: { - if (databaseId !== 0) { + if (databaseId === -1) { + realModel.initializeForNewRadio() + } else if (databaseId !== 0) { realModel.initializeByTrackId(databaseId) } else { realModel.initializeByTrackFileName(fileName) } } } + Connections{ + target: realModel + + onDisableApplyButton: applyButton.enabled = false + onShowDeleteButton: deleteButton.visible = true + onHideDeleteButton: deleteButton.visible = false + onCloseWindow: trackMetadata.rejected() + } + Component.onCompleted: { if (elisa.musicManager) { - if (databaseId !== 0) { + if (databaseId === -1) { + realModel.initializeForNewRadio() + } else if (databaseId !== 0) { realModel.initializeByTrackId(databaseId) } else { realModel.initializeByTrackFileName(fileName) diff --git a/src/qml/MetaDataDelegate.qml b/src/qml/MetaDataDelegate.qml --- a/src/qml/MetaDataDelegate.qml +++ b/src/qml/MetaDataDelegate.qml @@ -25,9 +25,17 @@ id: delegateRow spacing: 0 - height: (model.type === TrackMetadataModel.LongTextEntry ? longTextDisplayLoader.height : (metaDataLabelMetric.boundingRect.height + elisaTheme.layoutVerticalMargin / 2)) + property bool radio: false + + height: { + if(!delegateRow.radio){ + return (model.type === TrackMetadataModel.LongTextEntry ? longTextDisplayLoader.height : (metaDataLabelMetric.boundingRect.height + elisaTheme.layoutVerticalMargin / 2)) + } + } + signal radioEdited() + TextMetrics { id: metaDataLabelMetric @@ -37,7 +45,12 @@ Label { id: metaDataLabels - text: i18nc("Label for a piece of metadata, e.g. 'Album Artist:'", "%1:", model.name) + text: { + if(model.name !== undefined){ + return i18nc("Label for a piece of metadata, e.g. 'Album Artist:'", "%1:", model.name) + } + return "" + } font.weight: Font.Bold @@ -58,14 +71,39 @@ Layout.fillWidth: true Layout.alignment: Qt.AlignTop - sourceComponent: LabelWithToolTip { - text: model.display + Component{ + id: labelText - horizontalAlignment: Text.AlignLeft - elide: Text.ElideRight + LabelWithToolTip { + text: model.display - anchors.fill: parent + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + + anchors.fill: parent + } + } + + Component{ + id: fieldText + + TextField { + text: model.display + + horizontalAlignment: Text.AlignLeft + + anchors.fill: parent + + onEditingFinished: { + if(model.display !== text){ + model.display = text + delegateRow.radioEdited() + } + } + } } + + sourceComponent: (radio ? fieldText : labelText) } Loader { diff --git a/src/qml/NavigationActionBar.qml b/src/qml/NavigationActionBar.qml --- a/src/qml/NavigationActionBar.qml +++ b/src/qml/NavigationActionBar.qml @@ -29,6 +29,8 @@ property string secondaryTitle property url image property bool allowArtistNavigation: false + property bool showEnqueueButton: true + property bool showCreateRadioButton property string labelText property bool showRating: true @@ -42,6 +44,7 @@ signal enqueue(); signal replaceAndPlay(); + signal createRadio(); signal goBack(); signal showArtist(); signal sort(var order); @@ -68,6 +71,13 @@ onTriggered: enqueue() } + Action { + id: createRadioAction + text: i18nc("Create a new radio", "Create a radio") + icon.name: "media-track-add-amarok" + onTriggered: createRadio() + } + Action { id: showFilterAction shortcut: findAction.shortcut @@ -166,46 +176,63 @@ visible: secondaryTitle !== "" } }, + FlatButtonWithToolTip { + action: createRadioAction + objectName: 'createRadioButton' + + icon.height: elisaTheme.smallControlButtonSize + icon.width: elisaTheme.smallControlButtonSize + + focus: true + + visible: showCreateRadioButton + }, FlatButtonWithToolTip { action: enqueueAction objectName: 'enqueueButton' icon.height: elisaTheme.smallControlButtonSize icon.width: elisaTheme.smallControlButtonSize focus: true + + visible: !showCreateRadioButton }, FlatButtonWithToolTip { action: replaceAndPlayAction objectName: 'replaceAndPlayButton' icon.height: elisaTheme.smallControlButtonSize icon.width: elisaTheme.smallControlButtonSize + + visible: !showCreateRadioButton }, FlatButtonWithToolTip { action: showArtistAction objectName: 'showArtistButton' icon.height: elisaTheme.smallControlButtonSize icon.width: elisaTheme.smallControlButtonSize - visible: allowArtistNavigation + visible: allowArtistNavigation && !showCreateRadioButton }, FlatButtonWithToolTip { action: sortAction objectName: 'sortAscendingButton' icon.height: elisaTheme.smallControlButtonSize icon.width: elisaTheme.smallControlButtonSize - visible: enableSorting + visible: enableSorting && !showCreateRadioButton }, FlatButtonWithToolTip { action: showFilterAction objectName: 'showFilterButton' icon.height: elisaTheme.smallControlButtonSize icon.width: elisaTheme.smallControlButtonSize + + visible: !showCreateRadioButton } ] } diff --git a/src/trackslistener.cpp b/src/trackslistener.cpp --- a/src/trackslistener.cpp +++ b/src/trackslistener.cpp @@ -34,6 +34,8 @@ QSet mTracksByIdSet; + QSet mRadiosByIdSet; + QList> mTracksByNameSet; QList mTracksByFileNameSet; @@ -220,6 +222,16 @@ } break; } + case ElisaUtils::Radio: + { + d->mRadiosByIdSet.insert(newDatabaseId); + + auto newRadio = d->mDatabase->radioDataFromDatabaseId(newDatabaseId); + if (!newRadio.isEmpty()) { + Q_EMIT trackHasChanged(newRadio); + } + break; + } case ElisaUtils::Artist: newArtistInList(newDatabaseId, entryTitle); break; diff --git a/src/viewmanager.h b/src/viewmanager.h --- a/src/viewmanager.h +++ b/src/viewmanager.h @@ -44,7 +44,8 @@ FrequentlyPlayedTracks, RecentlyPlayedTracks, FilesBrowser, - Context + Context, + RadiosBrowser }; Q_ENUM(ViewsType) @@ -80,7 +81,8 @@ void openListView(ViewManager::ViewsType viewType, ElisaUtils::FilterType filterType, int expectedDepth, const QString &mainTitle, const QString &secondaryTitle, qulonglong databaseId, const QUrl &imageUrl, ElisaUtils::PlayListEntryType dataType, QVariant sortRole, - ViewManager::SortOrder sortOrder, bool displaySingleAlbum, ViewManager::AlbumViewStyle showDiscHeaders); + ViewManager::SortOrder sortOrder, bool displaySingleAlbum, ViewManager::AlbumViewStyle showDiscHeaders, + bool radioCase); void switchFilesBrowserView(ViewManager::ViewsType viewType, int expectedDepth, const QString &mainTitle, const QUrl &imageUrl); @@ -122,6 +124,8 @@ void openContextView(const QString &mainTitle, const QUrl &imageUrl); + void openRadiosBrowser(const QString &mainTitle, const QUrl &imageUrl); + void openOneAlbum(const QString &albumTitle, const QString &albumAuthor, const QUrl &albumCover, qulonglong albumDatabaseId, ViewManager::AlbumViewStyle albumDiscHeader); @@ -151,6 +155,8 @@ void contextViewIsLoaded(); + void radiosBrowserViewIsLoaded(); + QString mCurrentAlbumTitle; QString mCurrentAlbumAuthor; QString mCurrentArtistName; diff --git a/src/viewmanager.cpp b/src/viewmanager.cpp --- a/src/viewmanager.cpp +++ b/src/viewmanager.cpp @@ -51,6 +51,9 @@ case Context: openContextView(mainTitle, mainImage); break; + case RadiosBrowser: + openRadiosBrowser(mainTitle, mainImage); + break; case OneAlbum: case OneArtist: case OneAlbumFromArtist: @@ -80,6 +83,7 @@ case ElisaUtils::FileName: case ElisaUtils::Lyricist: case ElisaUtils::Composer: + case ElisaUtils::Radio: case ElisaUtils::Unknown: break; } @@ -131,6 +135,9 @@ case ViewsType::Context: contextViewIsLoaded(); break; + case ViewsType::RadiosBrowser: + radiosBrowserViewIsLoaded(); + break; } } @@ -141,7 +148,7 @@ if (mCurrentView != mTargetView) { Q_EMIT openListView(mTargetView, ElisaUtils::FilterByRecentlyPlayed, 1, mainTitle, {}, 0, imageUrl, ElisaUtils::Track, DatabaseInterface::LastPlayDate, - SortOrder::SortDescending, MultipleAlbum, NoDiscHeaders); + SortOrder::SortDescending, MultipleAlbum, NoDiscHeaders, false); } } @@ -152,7 +159,7 @@ if (mCurrentView != mTargetView) { Q_EMIT openListView(mTargetView, ElisaUtils::FilterByFrequentlyPlayed, 1, mainTitle, {}, 0, imageUrl, ElisaUtils::Track, DatabaseInterface::PlayFrequency, - SortOrder::SortDescending, MultipleAlbum, NoDiscHeaders); + SortOrder::SortDescending, MultipleAlbum, NoDiscHeaders, false); } } @@ -180,20 +187,20 @@ mTargetView = ViewsType::OneAlbum; Q_EMIT openListView(mTargetView, ElisaUtils::FilterById, 2, mTargetAlbumTitle, mTargetAlbumAuthor, mTargetDatabaseId, mTargetImageUrl, ElisaUtils::Track, {}, - SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader); + SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader, false); } else if (mCurrentView == ViewsType::OneArtist && mCurrentArtistName == mTargetAlbumAuthor) { mTargetView = ViewsType::OneAlbumFromArtist; Q_EMIT openListView(mTargetView, ElisaUtils::FilterById, 3, mTargetAlbumTitle, mTargetAlbumAuthor, mTargetDatabaseId, mTargetImageUrl, ElisaUtils::Track, {}, - SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader); + SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader, false); } else if (mCurrentView == ViewsType::OneArtist && mCurrentArtistName != mTargetAlbumAuthor) { mTargetView = ViewsType::OneAlbumFromArtist; Q_EMIT popOneView(); } else if (mCurrentView == ViewsType::OneArtistFromGenre) { mTargetView = ViewsType::OneAlbumFromArtistAndGenre; Q_EMIT openListView(mTargetView, ElisaUtils::FilterById, 4, mTargetAlbumTitle, mTargetAlbumAuthor, mTargetDatabaseId, mTargetImageUrl, ElisaUtils::Track, {}, - SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader); + SortOrder::NoSort, SingleAlbum, mAlbumDiscHeader, false); } else { mTargetView = ViewsType::OneAlbum; Q_EMIT openGridView(ViewsType::AllAlbums, ElisaUtils::NoFilter, 1, {}, {}, {}, ElisaUtils::Album, @@ -249,7 +256,7 @@ if (mCurrentView != mTargetView) { Q_EMIT openListView(mTargetView, ElisaUtils::NoFilter, 1, mainTitle, {}, 0, imageUrl, ElisaUtils::Track, Qt::DisplayRole, - SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders); + SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders, false); } } @@ -291,7 +298,16 @@ if (mCurrentView != mTargetView) { Q_EMIT switchContextView(mTargetView, 1, mainTitle, imageUrl); } +} +void ViewManager::openRadiosBrowser(const QString &mainTitle, const QUrl &imageUrl) +{ + mTargetView = ViewsType::RadiosBrowser; + if (mCurrentView != mTargetView) { + Q_EMIT openListView(mTargetView, ElisaUtils::NoFilter, 1, mainTitle, {}, + 0, imageUrl, ElisaUtils::Radio, Qt::DisplayRole, + SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders, true); + } } void ViewManager::recentlyPlayedTracksIsLoaded() @@ -310,7 +326,7 @@ if (mTargetView == ViewsType::OneAlbum) { Q_EMIT openListView(mTargetView, ElisaUtils::FilterById, 2, mTargetAlbumTitle, mTargetAlbumAuthor, mTargetDatabaseId, mTargetImageUrl, ElisaUtils::Track, Qt::DisplayRole, - SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders); + SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders, false); } } @@ -353,7 +369,7 @@ Q_EMIT openListView(mTargetView, ElisaUtils::FilterById, 3, mTargetAlbumTitle, mTargetAlbumAuthor, mTargetDatabaseId, mTargetImageUrl, ElisaUtils::Track, Qt::DisplayRole, - SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders); + SortOrder::SortAscending, MultipleAlbum, NoDiscHeaders, false); } } @@ -383,6 +399,11 @@ mCurrentView = ViewsType::Context; } +void ViewManager::radiosBrowserViewIsLoaded() +{ + mCurrentView = ViewsType::RadiosBrowser; +} + void ViewManager::goBack() { Q_EMIT popOneView();