diff options
Diffstat (limited to '')
91 files changed, 34747 insertions, 0 deletions
diff --git a/xbmc/music/Album.cpp b/xbmc/music/Album.cpp new file mode 100644 index 0000000..efaa7ac --- /dev/null +++ b/xbmc/music/Album.cpp @@ -0,0 +1,671 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Album.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/MathUtils.h" +#include "utils/StringUtils.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <algorithm> + +using namespace MUSIC_INFO; + +typedef struct ReleaseTypeInfo { + CAlbum::ReleaseType type; + std::string name; +} ReleaseTypeInfo; + +ReleaseTypeInfo releaseTypes[] = { + { CAlbum::Album, "album" }, + { CAlbum::Single, "single" } +}; + +CAlbum::CAlbum(const CFileItem& item) +{ + Reset(); + const CMusicInfoTag& tag = *item.GetMusicInfoTag(); + strAlbum = tag.GetAlbum(); + strMusicBrainzAlbumID = tag.GetMusicBrainzAlbumID(); + strReleaseGroupMBID = tag.GetMusicBrainzReleaseGroupID(); + genre = tag.GetGenre(); + strArtistDesc = tag.GetAlbumArtistString(); + //Set sort string before processing artist credits + strArtistSort = tag.GetAlbumArtistSort(); + // Determine artist credits from various tag arrays, inc fallback to song artist names + SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(), + tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID()); + + strOrigReleaseDate = tag.GetOriginalDate(); + strReleaseDate = tag.GetReleaseDate(); + strLabel = tag.GetRecordLabel(); + strType = tag.GetMusicBrainzReleaseType(); + bCompilation = tag.GetCompilation(); + iTimesPlayed = 0; + bBoxedSet = tag.GetBoxset(); + dateAdded.Reset(); + dateUpdated.Reset(); + lastPlayed.Reset(); + releaseType = tag.GetAlbumReleaseType(); + strReleaseStatus = tag.GetAlbumReleaseStatus(); +} + +void CAlbum::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints, + const std::vector<std::string>& mbids, + const std::vector<std::string>& artistnames, const std::vector<std::string>& artisthints, + const std::vector<std::string>& artistmbids) +{ + std::vector<std::string> albumartistHints = hints; + //Split the artist sort string to try and get sort names for individual artists + auto artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + artistCredits.clear(); + + if (!mbids.empty()) + { // Have musicbrainz artist info, so use it + + // Vector of possible separators in the order least likely to be part of artist name + const std::vector<std::string> separators{ " feat. ", ";", ":", "|", "#", "/", ",", "&" }; + + // Establish tag consistency + // Do the number of musicbrainz ids and *both* number of names and number of hints mismatch? + if (albumartistHints.size() != mbids.size() && names.size() != mbids.size()) + { + // Tags mismatch - report it and then try to fix + CLog::Log(LOGDEBUG, "Mismatch in song file albumartist tags: {} mbid {} name album: {} {}", + (int)mbids.size(), (int)names.size(), strAlbum, strArtistDesc); + /* + Most likely we have no hints and a single artist name like "Artist1 feat. Artist2" + or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the + expected single item separator (default = space-slash-space) as not been used. + Comma and slash (no spaces) are poor delimiters as could be in name e.g. AC/DC, + but here treat them as such in attempt to find artist names. + When there are hints they could be poorly formatted using unexpected separators, + so attempt to split them. Or we could have more hints or artist names than + musicbrainz so ignore them but raise warning. + */ + + // Do hints exist yet mismatch + if (!albumartistHints.empty() && albumartistHints.size() != mbids.size()) + { + if (names.size() == mbids.size()) + // Album artist name count matches, use that as hints + albumartistHints = names; + else if (albumartistHints.size() < mbids.size()) + { // Try splitting the hints until have matching number + albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size()); + } + else + // Extra hints, discard them. + albumartistHints.resize(mbids.size()); + } + // Do hints not exist or still mismatch, try album artists + if (albumartistHints.size() != mbids.size()) + albumartistHints = names; + // Still mismatch, try splitting the hints (now artists) until have matching number + if (albumartistHints.size() < mbids.size()) + albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size()); + // Try matching on artists or artist hints field, if it is reliable + if (albumartistHints.size() != mbids.size()) + { + if (!artistmbids.empty() && + (artistmbids.size() == artistnames.size() || + artistmbids.size() == artisthints.size())) + { + for (size_t i = 0; i < mbids.size(); i++) + { + for (size_t j = 0; j < artistmbids.size(); j++) + { + if (mbids[i] == artistmbids[j]) + { + if (albumartistHints.size() < i + 1) + albumartistHints.resize(i + 1); + if (artistmbids.size() == artisthints.size()) + albumartistHints[i] = artisthints[j]; + else + albumartistHints[i] = artistnames[j]; + } + } + } + } + } + } + else + { // Either hints or album artists (or both) name matches number of musicbrainz id + // If hints mismatch, use album artists + if (albumartistHints.size() != mbids.size()) + albumartistHints = names; + } + + // Try to get number of artist sort names and musicbrainz ids to match. Split sort names + // further using multiple possible delimiters, over single separator applied in Tag loader + if (artistSort.size() != mbids.size()) + artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" }); + + for (size_t i = 0; i < mbids.size(); i++) + { + std::string artistId = mbids[i]; + std::string artistName; + /* + We try and get the musicbrainz id <-> name matching from the hints and match on the same index. + Some album artist hints could be blank (if populated from artist or artist hints). + If not found, use the musicbrainz id and hope we later on can update that entry. + If we have more names than musicbrainz id they are ignored, but raise a warning. + */ + if (i < albumartistHints.size()) + artistName = albumartistHints[i]; + if (artistName.empty()) + artistName = artistId; + + // Use artist sort name providing we have as many as we have mbid, + // otherwise something is wrong with them so ignore and leave blank + if (artistSort.size() == mbids.size()) + artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId); + else + artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId); + } + } + else + { + /* + No musicbrainz album artist ids so fill artist names directly. + This method only called during scanning when there is a musicbrainz album id, so + means mbid tags are incomplete. But could also be called by JSON to SetAlbumDetails + Try to separate album artist names further, and trim blank space. + */ + std::vector<std::string> albumArtists = names; + if (albumartistHints.size() > albumArtists.size()) + // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names + albumArtists = albumartistHints; + else + // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader + albumArtists = StringUtils::SplitMulti(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators); + + if (artistSort.size() != albumArtists.size()) + // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader + artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" }); + + for (size_t i = 0; i < albumArtists.size(); i++) + { + artistCredits.emplace_back(StringUtils::Trim(albumArtists[i])); + // Set artist sort name providing we have as many as we have artists, + // otherwise something is wrong with them so ignore rather than guess. + if (artistSort.size() == albumArtists.size()) + artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i])); + } + } +} + +void CAlbum::MergeScrapedAlbum(const CAlbum& source, bool override /* = true */) +{ + /* + Initial scraping of album information when there is a Musicbrainz album ID derived from + tags is done directly using that ID, otherwise the lookup is based on album and artist names + but this can sometimes mis-identify the album (i.e. classical music has many "Symphony No. 5"). + It is useful to store the scraped mbid, but we need to be able to correct any mistakes. Hence + a manual refresh of album information uses either the mbid as derived from tags or the album + and artist names, not any previously scraped mbid. + + When overwriting the data derived from tags, AND the original and scraped album have the same + Musicbrainz album ID, then merging is used to keep Kodi up to date with changes in the Musicbrainz + database including album artist credits, song artist credits and song titles. However it is only + appropriate when the music files are tagged with mbids, these are taken as definative, scraped + mbids can not be depended on in this way. + + When the album is megerd in this deep way it is flagged so that the database album update is aware + artist credits and songs need to be updated too. + */ + + bArtistSongMerge = override && !bScrapedMBID + && !source.strMusicBrainzAlbumID.empty() && !strMusicBrainzAlbumID.empty() + && (strMusicBrainzAlbumID.compare(source.strMusicBrainzAlbumID) == 0); + + /* + Musicbrainz album (release) ID and release group ID values derived from music file tags are + always taken as accurate and so can not be overwritten by a scraped value. When the album does + not already have an mbid or has a previously scraped mbid, merge the new scraped value, + flagging it as being from the scraper rather than derived from music file tags. + */ + if (!source.strMusicBrainzAlbumID.empty() && (strMusicBrainzAlbumID.empty() || bScrapedMBID)) + { + strMusicBrainzAlbumID = source.strMusicBrainzAlbumID; + bScrapedMBID = true; + } + if (!source.strReleaseGroupMBID.empty() && (strReleaseGroupMBID.empty() || bScrapedMBID)) + { + strReleaseGroupMBID = source.strReleaseGroupMBID; + } + + /* + Scraping can return different album artists from the originals derived from tags, even when + doing a lookup on artist name. + + When overwriting the data derived from tags, AND the original and scraped album have the same + Musicbrainz album ID, then merging an album replaces both the album artsts and the song artists + with those scraped (providing they are not empty). + + When not doing that kind of merge, for any matching artist names the Musicbrainz artist id + returned by the scraper can be used to populate any previously missing Musicbrainz artist id values. + */ + if (bArtistSongMerge && !source.artistCredits.empty()) + { + artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper + strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 & artist2" when scraped + } + else + { + // Compare original album artists with those scraped (ignoring order), and set any missing mbid + for (auto &artistCredit : artistCredits) + { + if (artistCredit.GetMusicBrainzArtistID().empty()) + { + for (const auto& sourceartistCredit : source.artistCredits) + { + if (StringUtils::EqualsNoCase(artistCredit.GetArtist(), sourceartistCredit.GetArtist())) + { + artistCredit.SetMusicBrainzArtistID(sourceartistCredit.GetMusicBrainzArtistID()); + artistCredit.SetScrapedMBID(true); + break; + } + } + } + } + } + + //@todo: scraped album genre needs adding to genre and album_genre tables, this just changes the string + if ((override && !source.genre.empty()) || genre.empty()) + genre = source.genre; + if ((override && !source.strAlbum.empty()) || strAlbum.empty()) + strAlbum = source.strAlbum; + //@todo: validate ISO8601 format YYYY, YYYY-MM, or YYYY-MM-DD + if ((override && !source.strReleaseDate.empty()) || strReleaseDate.empty()) + strReleaseDate = source.strReleaseDate; + if ((override && !source.strOrigReleaseDate.empty()) || strOrigReleaseDate.empty()) + strOrigReleaseDate = source.strOrigReleaseDate; + + if (override) + bCompilation = source.bCompilation; + // iTimesPlayed = source.iTimesPlayed; // times played is derived from songs + + if ((override && !source.strArtistSort.empty()) || strArtistSort.empty()) + strArtistSort = source.strArtistSort; + for (const auto& i : source.art) + { + if (override || art.find(i.first) == art.end()) + art[i.first] = i.second; + } + if((override && !source.strLabel.empty()) || strLabel.empty()) + strLabel = source.strLabel; + thumbURL = source.thumbURL; + moods = source.moods; + styles = source.styles; + themes = source.themes; + strReview = source.strReview; + if ((override && !source.strType.empty()) || strType.empty()) + strType = source.strType; +// strPath = source.strPath; // don't merge the path + if ((override && !source.strReleaseStatus.empty()) || strReleaseStatus.empty()) + strReleaseStatus = source.strReleaseStatus; + fRating = source.fRating; + iUserrating = source.iUserrating; + iVotes = source.iVotes; + + /* + When overwriting the data derived from tags, AND the original and scraped album have the same + Musicbrainz album ID, update the local songs with scaped Musicbrainz information including the + artist credits. + */ + if (bArtistSongMerge) + { + for (auto &song : songs) + { + if (!song.strMusicBrainzTrackID.empty()) + for (const auto& sourceSong : source.songs) + if ((sourceSong.strMusicBrainzTrackID == song.strMusicBrainzTrackID) && (sourceSong.iTrack == song.iTrack)) + song.MergeScrapedSong(sourceSong, override); + } + } +} + +std::string CAlbum::GetGenreString() const +{ + return StringUtils::Join(genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); +} + +const std::vector<std::string> CAlbum::GetAlbumArtist() const +{ + //Get artist names as vector from artist credits + std::vector<std::string> albumartists; + for (const auto& artistCredit : artistCredits) + { + albumartists.push_back(artistCredit.GetArtist()); + } + return albumartists; +} + +const std::vector<std::string> CAlbum::GetMusicBrainzAlbumArtistID() const +{ + //Get artist MusicBrainz IDs as vector from artist credits + std::vector<std::string> musicBrainzID; + for (const auto& artistCredit : artistCredits) + { + musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID()); + } + return musicBrainzID; +} + +const std::string CAlbum::GetAlbumArtistString() const +{ + //Artist description may be different from the artists in artistcredits (see ALBUMARTISTS tag processing) + //but is takes precedence as a string because artistcredits is not always filled during processing + if (!strArtistDesc.empty()) + return strArtistDesc; + std::vector<std::string> artistvector; + for (const auto& i : artistCredits) + artistvector.emplace_back(i.GetArtist()); + std::string artistString; + if (!artistvector.empty()) + artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + return artistString; +} + +const std::string CAlbum::GetAlbumArtistSort() const +{ + //The stored artist sort name string takes precedence but a + //value could be created from individual sort names held in artistcredits + if (!strArtistSort.empty()) + return strArtistSort; + std::vector<std::string> artistvector; + for (const auto& artistcredit : artistCredits) + if (!artistcredit.GetSortName().empty()) + artistvector.emplace_back(artistcredit.GetSortName()); + std::string artistString; + if (!artistvector.empty()) + artistString = StringUtils::Join(artistvector, "; "); + return artistString; +} + +const std::vector<int> CAlbum::GetArtistIDArray() const +{ + // Get album artist IDs for json rpc + std::vector<int> artistids; + for (const auto& artistCredit : artistCredits) + artistids.push_back(artistCredit.GetArtistId()); + return artistids; +} + + +std::string CAlbum::GetReleaseType() const +{ + return ReleaseTypeToString(releaseType); +} + +void CAlbum::SetReleaseType(const std::string& strReleaseType) +{ + releaseType = ReleaseTypeFromString(strReleaseType); +} + +void CAlbum::SetDateAdded(const std::string& strDateAdded) +{ + dateAdded.SetFromDBDateTime(strDateAdded); +} + +void CAlbum::SetDateUpdated(const std::string& strDateUpdated) +{ + dateUpdated.SetFromDBDateTime(strDateUpdated); +} + +void CAlbum::SetDateNew(const std::string& strDateNew) +{ + dateNew.SetFromDBDateTime(strDateNew); +} + +void CAlbum::SetLastPlayed(const std::string& strLastPlayed) +{ + lastPlayed.SetFromDBDateTime(strLastPlayed); +} + +std::string CAlbum::ReleaseTypeToString(CAlbum::ReleaseType releaseType) +{ + for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes) + { + if (releaseTypeInfo.type == releaseType) + return releaseTypeInfo.name; + } + + return "album"; +} + +CAlbum::ReleaseType CAlbum::ReleaseTypeFromString(const std::string& strReleaseType) +{ + for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes) + { + if (releaseTypeInfo.name == strReleaseType) + return releaseTypeInfo.type; + } + + return Album; +} + +bool CAlbum::operator<(const CAlbum &a) const +{ + if (strMusicBrainzAlbumID.empty() && a.strMusicBrainzAlbumID.empty()) + { + if (strAlbum < a.strAlbum) return true; + if (strAlbum > a.strAlbum) return false; + + // This will do an std::vector compare (i.e. item by item) + if (GetAlbumArtist() < a.GetAlbumArtist()) return true; + if (GetAlbumArtist() > a.GetAlbumArtist()) return false; + return false; + } + + if (strMusicBrainzAlbumID < a.strMusicBrainzAlbumID) return true; + if (strMusicBrainzAlbumID > a.strMusicBrainzAlbumID) return false; + return false; +} + +bool CAlbum::Load(const TiXmlElement *album, bool append, bool prioritise) +{ + if (!album) return false; + if (!append) + Reset(); + + const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + XMLUtils::GetString(album, "title", strAlbum); + XMLUtils::GetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID); + XMLUtils::GetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID); + XMLUtils::GetBoolean(album, "scrapedmbid", bScrapedMBID); + XMLUtils::GetString(album, "artistdesc", strArtistDesc); + std::vector<std::string> artist; // Support old style <artist></artist> for backwards compatibility + XMLUtils::GetStringArray(album, "artist", artist, prioritise, itemSeparator); + XMLUtils::GetStringArray(album, "genre", genre, prioritise, itemSeparator); + XMLUtils::GetStringArray(album, "style", styles, prioritise, itemSeparator); + XMLUtils::GetStringArray(album, "mood", moods, prioritise, itemSeparator); + XMLUtils::GetStringArray(album, "theme", themes, prioritise, itemSeparator); + XMLUtils::GetBoolean(album, "compilation", bCompilation); + XMLUtils::GetBoolean(album, "boxset", bBoxedSet); + + XMLUtils::GetString(album,"review",strReview); + XMLUtils::GetString(album,"label",strLabel); + XMLUtils::GetInt(album, "duration", iAlbumDuration); + XMLUtils::GetString(album,"type",strType); + XMLUtils::GetString(album, "releasestatus", strReleaseStatus); + + XMLUtils::GetString(album, "releasedate", strReleaseDate); + StringUtils::Trim(strReleaseDate); // @todo: validate ISO8601 format + // Support old style <year></year> for backwards compatibility + if (strReleaseDate.empty()) + { + int year; + XMLUtils::GetInt(album, "year", year); + if (year > 0) + strReleaseDate = StringUtils::Format("{:04}", year); + } + XMLUtils::GetString(album, "originalreleasedate", strOrigReleaseDate); + + const TiXmlElement* rElement = album->FirstChildElement("rating"); + if (rElement) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(album, "rating", rating); + if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating>=1) + rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + fRating = rating; + } + const TiXmlElement* userrating = album->FirstChildElement("userrating"); + if (userrating) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(album, "userrating", rating); + if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1) + rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + iUserrating = MathUtils::round_int(static_cast<double>(rating)); + } + XMLUtils::GetInt(album, "votes", iVotes); + + size_t iThumbCount = thumbURL.GetUrls().size(); + std::string xmlAdd = thumbURL.GetData(); + const TiXmlElement* thumb = album->FirstChildElement("thumb"); + while (thumb) + { + thumbURL.ParseAndAppendUrl(thumb); + if (prioritise) + { + std::string temp; + temp << *thumb; + xmlAdd = temp+xmlAdd; + } + thumb = thumb->NextSiblingElement("thumb"); + } + // prioritise thumbs from nfos + if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size()) + { + auto thumbUrls = thumbURL.GetUrls(); + rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end()); + thumbURL.SetUrls(thumbUrls); + thumbURL.SetData(xmlAdd); + } + + const TiXmlElement* albumArtistCreditsNode = album->FirstChildElement("albumArtistCredits"); + if (albumArtistCreditsNode) + artistCredits.clear(); + + while (albumArtistCreditsNode) + { + if (albumArtistCreditsNode->FirstChild()) + { + CArtistCredit artistCredit; + XMLUtils::GetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist); + XMLUtils::GetString(albumArtistCreditsNode, "musicBrainzArtistID", artistCredit.m_strMusicBrainzArtistID); + artistCredits.push_back(artistCredit); + } + + albumArtistCreditsNode = albumArtistCreditsNode->NextSiblingElement("albumArtistCredits"); + } + + // Support old style <artist></artist> for backwards compatibility + // .nfo files should ideally be updated to use the artist credits structure above + // or removed entirely in preference for better tags (MusicBrainz?) + if (artistCredits.empty() && !artist.empty()) + { + for (const auto& it : artist) + { + CArtistCredit artistCredit(it); + artistCredits.push_back(artistCredit); + } + } + + std::string strReleaseType; + if (XMLUtils::GetString(album, "releasetype", strReleaseType)) + SetReleaseType(strReleaseType); + else + releaseType = Album; + + return true; +} + +bool CAlbum::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath) +{ + if (!node) return false; + + // we start with a <tag> tag + TiXmlElement albumElement(tag.c_str()); + TiXmlNode *album = node->InsertEndChild(albumElement); + + if (!album) return false; + + XMLUtils::SetString(album, "title", strAlbum); + XMLUtils::SetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID); + XMLUtils::SetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID); + XMLUtils::SetBoolean(album, "scrapedmbid", bScrapedMBID); + XMLUtils::SetString(album, "artistdesc", strArtistDesc); //Can be different from artist credits + XMLUtils::SetStringArray(album, "genre", genre); + XMLUtils::SetStringArray(album, "style", styles); + XMLUtils::SetStringArray(album, "mood", moods); + XMLUtils::SetStringArray(album, "theme", themes); + XMLUtils::SetBoolean(album, "compilation", bCompilation); + XMLUtils::SetBoolean(album, "boxset", bBoxedSet); + + XMLUtils::SetString(album, "review", strReview); + XMLUtils::SetString(album, "type", strType); + XMLUtils::SetString(album, "releasestatus", strReleaseStatus); + XMLUtils::SetString(album, "releasedate", strReleaseDate); + XMLUtils::SetString(album, "originalreleasedate", strOrigReleaseDate); + XMLUtils::SetString(album, "label", strLabel); + XMLUtils::SetInt(album, "duration", iAlbumDuration); + if (thumbURL.HasData()) + { + CXBMCTinyXML doc; + doc.Parse(thumbURL.GetData()); + const TiXmlNode* thumb = doc.FirstChild("thumb"); + while (thumb) + { + album->InsertEndChild(*thumb); + thumb = thumb->NextSibling("thumb"); + } + } + XMLUtils::SetString(album, "path", strPath); + + auto* rating = XMLUtils::SetFloat(album, "rating", fRating); + if (rating) + rating->ToElement()->SetAttribute("max", 10); + + auto* userrating = XMLUtils::SetInt(album, "userrating", iUserrating); + if (userrating) + userrating->ToElement()->SetAttribute("max", 10); + + XMLUtils::SetInt(album, "votes", iVotes); + + for (const auto& artistCredit : artistCredits) + { + // add an <albumArtistCredits> tag + TiXmlElement albumArtistCreditsElement("albumArtistCredits"); + TiXmlNode *albumArtistCreditsNode = album->InsertEndChild(albumArtistCreditsElement); + XMLUtils::SetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist); + XMLUtils::SetString(albumArtistCreditsNode, "musicBrainzArtistID", + artistCredit.m_strMusicBrainzArtistID); + } + + XMLUtils::SetString(album, "releasetype", GetReleaseType()); + + return true; +} + diff --git a/xbmc/music/Album.h b/xbmc/music/Album.h new file mode 100644 index 0000000..18a08b7 --- /dev/null +++ b/xbmc/music/Album.h @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +/*! + \file Album.h +\brief +*/ + +#include "Artist.h" +#include "Song.h" +#include "XBDateTime.h" +#include "utils/ScraperUrl.h" + +#include <map> +#include <vector> + +class TiXmlNode; +class CFileItem; +class CAlbum +{ +public: + explicit CAlbum(const CFileItem& item); + CAlbum() = default; + bool operator<(const CAlbum &a) const; + void MergeScrapedAlbum(const CAlbum& album, bool override = true); + + void Reset() + { + idAlbum = -1; + strAlbum.clear(); + strMusicBrainzAlbumID.clear(); + strReleaseGroupMBID.clear(); + artistCredits.clear(); + strArtistDesc.clear(); + strArtistSort.clear(); + genre.clear(); + thumbURL.Clear(); + moods.clear(); + styles.clear(); + themes.clear(); + art.clear(); + strReview.clear(); + strLabel.clear(); + strType.clear(); + strReleaseStatus.clear(); + strPath.clear(); + fRating = -1; + iUserrating = -1; + iVotes = -1; + strOrigReleaseDate.clear(); + strReleaseDate.clear(); + bCompilation = false; + bBoxedSet = false; + iTimesPlayed = 0; + dateAdded.Reset(); + dateUpdated.Reset(); + dateNew.Reset(); + lastPlayed.Reset(); + iTotalDiscs = -1; + songs.clear(); + releaseType = Album; + strLastScraped.clear(); + bScrapedMBID = false; + bArtistSongMerge = false; + iAlbumDuration = 0; + } + + /*! \brief Get album artist names from the vector of artistcredits objects + \return album artist names as a vector of strings + */ + const std::vector<std::string> GetAlbumArtist() const; + + /*! \brief Get album artist MusicBrainz IDs from the vector of artistcredits objects + \return album artist MusicBrainz IDs as a vector of strings + */ + const std::vector<std::string> GetMusicBrainzAlbumArtistID() const; + std::string GetGenreString() const; + + /*! \brief Get album artist names from the artist description string (if it exists) + or concatenated from the vector of artistcredits objects + \return album artist names as a single string + */ + const std::string GetAlbumArtistString() const; + + /*! \brief Get album artist sort name from the artist sort string (if it exists) + or concatenated from the vector of artistcredits objects + \return album artist sort names as a single string + */ + const std::string GetAlbumArtistSort() const; + + /*! \brief Get album artist IDs (for json rpc) from the vector of artistcredits objects + \return album artist IDs as a vector of integers + */ + const std::vector<int> GetArtistIDArray() const; + + typedef enum ReleaseType { + Album = 0, + Single + } ReleaseType; + + std::string GetReleaseType() const; + void SetReleaseType(const std::string& strReleaseType); + void SetDateAdded(const std::string& strDateAdded); + void SetDateUpdated(const std::string& strDateUpdated); + void SetDateNew(const std::string& strDateNew); + void SetLastPlayed(const std::string& strLastPlayed); + + static std::string ReleaseTypeToString(ReleaseType releaseType); + static ReleaseType ReleaseTypeFromString(const std::string& strReleaseType); + + /*! \brief Set album artist credits using the arrays of tag values. + If strArtistSort (as from ALBUMARTISTSORT tag) is already set then individual + artist sort names are also processed. + \param names String vector of albumartist names (as from ALBUMARTIST tag) + \param hints String vector of albumartist name hints (as from ALBUMARTISTS tag) + \param mbids String vector of albumartist Musicbrainz IDs (as from MUSICBRAINZABUMARTISTID tag) + \param artistnames String vector of artist names (as from ARTIST tag) + \param artisthints String vector of artist name hints (as from ARTISTS tag) + \param artistmbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag) + */ + void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints, + const std::vector<std::string>& mbids, + const std::vector<std::string>& artistnames = std::vector<std::string>(), + const std::vector<std::string>& artisthints = std::vector<std::string>(), + const std::vector<std::string>& artistmbids = std::vector<std::string>()); + + /*! \brief Load album information from an XML file. + See CVideoInfoTag::Load for a description of the types of elements we load. + \param element the root XML element to parse. + \param append whether information should be added to the existing tag, or whether it should be reset first. + \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false. + \sa CVideoInfoTag::Load + */ + bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false); + bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath); + + int idAlbum = -1; + std::string strAlbum; + std::string strMusicBrainzAlbumID; + std::string strReleaseGroupMBID; + std::string strArtistDesc; + std::string strArtistSort; + VECARTISTCREDITS artistCredits; + std::vector<std::string> genre; + CScraperUrl thumbURL; + std::vector<std::string> moods; + std::vector<std::string> styles; + std::vector<std::string> themes; + std::map<std::string, std::string> art; + std::string strReview; + std::string strLabel; + std::string strType; + std::string strReleaseStatus; + std::string strPath; + float fRating = -1; + int iUserrating = -1; + int iVotes = -1; + std::string strReleaseDate; + std::string strOrigReleaseDate; + bool bBoxedSet = false; + bool bCompilation = false; + int iTimesPlayed = 0; + CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned + CDateTime dateUpdated; // Time db record Last modified + CDateTime dateNew; // Time db record created + CDateTime lastPlayed; + int iTotalDiscs = -1; + VECSONGS songs; ///< Local songs + ReleaseType releaseType = Album; + std::string strLastScraped; + bool bScrapedMBID = false; + bool bArtistSongMerge = false; + int iAlbumDuration = 0; +}; + +typedef std::vector<CAlbum> VECALBUMS; diff --git a/xbmc/music/Artist.cpp b/xbmc/music/Artist.cpp new file mode 100644 index 0000000..786f7a7 --- /dev/null +++ b/xbmc/music/Artist.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Artist.h" + +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/Fanart.h" +#include "utils/XMLUtils.h" + +#include <algorithm> + +void CArtist::MergeScrapedArtist(const CArtist& source, bool override /* = true */) +{ + /* + Initial scraping of artist information when the mbid is derived from tags is done directly + using that ID, otherwise the lookup is based on name and can mis-identify the artist + (many have same name). It is useful to store the scraped mbid, but we need to be + able to correct any mistakes. Hence a manual refresh of artist information uses either + the mbid is derived from tags or the artist name, not any previously scraped mbid. + + A Musicbrainz artist ID derived from music file tags is always taken as accurate and so can + not be overwritten by a scraped value. When the artist does not already have an mbid or has + a previously scraped mbid, merge the new scraped value, flagging it as being from the + scraper rather than derived from music file tags. + */ + if (!source.strMusicBrainzArtistID.empty() && (strMusicBrainzArtistID.empty() || bScrapedMBID)) + { + strMusicBrainzArtistID = source.strMusicBrainzArtistID; + bScrapedMBID = true; + } + + if ((override && !source.strArtist.empty()) || strArtist.empty()) + strArtist = source.strArtist; + + if ((override && !source.strSortName.empty()) || strSortName.empty()) + strSortName = source.strSortName; + + strType = source.strType; + strGender = source.strGender; + strDisambiguation = source.strDisambiguation; + genre = source.genre; + strBiography = source.strBiography; + styles = source.styles; + moods = source.moods; + instruments = source.instruments; + strBorn = source.strBorn; + strFormed = source.strFormed; + strDied = source.strDied; + strDisbanded = source.strDisbanded; + yearsActive = source.yearsActive; + + thumbURL = source.thumbURL; // Available remote art + // Current artwork - thumb, fanart etc., to be stored in art table + if (!source.art.empty()) + art = source.art; + + discography = source.discography; +} + + +bool CArtist::Load(const TiXmlElement *artist, bool append, bool prioritise) +{ + if (!artist) return false; + if (!append) + Reset(); + + const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + XMLUtils::GetString(artist, "name", strArtist); + XMLUtils::GetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID); + XMLUtils::GetString(artist, "sortname", strSortName); + XMLUtils::GetString(artist, "type", strType); + XMLUtils::GetString(artist, "gender", strGender); + XMLUtils::GetString(artist, "disambiguation", strDisambiguation); + XMLUtils::GetStringArray(artist, "genre", genre, prioritise, itemSeparator); + XMLUtils::GetStringArray(artist, "style", styles, prioritise, itemSeparator); + XMLUtils::GetStringArray(artist, "mood", moods, prioritise, itemSeparator); + XMLUtils::GetStringArray(artist, "yearsactive", yearsActive, prioritise, itemSeparator); + XMLUtils::GetStringArray(artist, "instruments", instruments, prioritise, itemSeparator); + + XMLUtils::GetString(artist, "born", strBorn); + XMLUtils::GetString(artist, "formed", strFormed); + XMLUtils::GetString(artist, "biography", strBiography); + XMLUtils::GetString(artist, "died", strDied); + XMLUtils::GetString(artist, "disbanded", strDisbanded); + + size_t iThumbCount = thumbURL.GetUrls().size(); + std::string xmlAdd = thumbURL.GetData(); + + // Available artist thumbs + const TiXmlElement* thumb = artist->FirstChildElement("thumb"); + while (thumb) + { + thumbURL.ParseAndAppendUrl(thumb); + if (prioritise) + { + std::string temp; + temp << *thumb; + xmlAdd = temp+xmlAdd; + } + thumb = thumb->NextSiblingElement("thumb"); + } + // prefix thumbs from nfos + if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size()) + { + auto thumbUrls = thumbURL.GetUrls(); + rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end()); + thumbURL.SetUrls(thumbUrls); + thumbURL.SetData(xmlAdd); + } + + // Discography + const TiXmlElement* node = artist->FirstChildElement("album"); + if (node) + discography.clear(); + while (node) + { + if (node->FirstChild()) + { + CDiscoAlbum album; + XMLUtils::GetString(node, "title", album.strAlbum); + XMLUtils::GetString(node, "year", album.strYear); + XMLUtils::GetString(node, "musicbrainzreleasegroupid", album.strReleaseGroupMBID); + discography.push_back(album); + } + node = node->NextSiblingElement("album"); + } + + // Support old style <fanart></fanart> for backwards compatibility of old nfo files and scrapers + const TiXmlElement *fanart2 = artist->FirstChildElement("fanart"); + if (fanart2) + { + CFanart fanart; + // we prefix to handle mixed-mode nfo's with fanart set + if (prioritise) + { + std::string temp; + temp << *fanart2; + fanart.m_xml = temp+fanart.m_xml; + } + else + fanart.m_xml << *fanart2; + fanart.Unpack(); + // Append fanart to other image URLs + for (unsigned int i = 0; i < fanart.GetNumFanarts(); i++) + thumbURL.AddParsedUrl(fanart.GetImageURL(i), "fanart", fanart.GetPreviewURL(i)); + } + + // Current artwork - thumb, fanart etc. (the chosen art, not the lists of those available) + node = artist->FirstChildElement("art"); + if (node) + { + const TiXmlNode *artdetailNode = node->FirstChild(); + while (artdetailNode && artdetailNode->FirstChild()) + { + art.insert(make_pair(artdetailNode->ValueStr(), artdetailNode->FirstChild()->ValueStr())); + artdetailNode = artdetailNode->NextSibling(); + } + } + + return true; +} + +bool CArtist::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath) +{ + if (!node) return false; + + // we start with a <tag> tag + TiXmlElement artistElement(tag.c_str()); + TiXmlNode *artist = node->InsertEndChild(artistElement); + + if (!artist) return false; + + XMLUtils::SetString(artist, "name", strArtist); + XMLUtils::SetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID); + XMLUtils::SetString(artist, "sortname", strSortName); + XMLUtils::SetString(artist, "type", strType); + XMLUtils::SetString(artist, "gender", strGender); + XMLUtils::SetString(artist, "disambiguation", strDisambiguation); + XMLUtils::SetStringArray(artist, "genre", genre); + XMLUtils::SetStringArray(artist, "style", styles); + XMLUtils::SetStringArray(artist, "mood", moods); + XMLUtils::SetStringArray(artist, "yearsactive", yearsActive); + XMLUtils::SetStringArray(artist, "instruments", instruments); + XMLUtils::SetString(artist, "born", strBorn); + XMLUtils::SetString(artist, "formed", strFormed); + XMLUtils::SetString(artist, "biography", strBiography); + XMLUtils::SetString(artist, "died", strDied); + XMLUtils::SetString(artist, "disbanded", strDisbanded); + // Available remote art + if (thumbURL.HasData()) + { + CXBMCTinyXML doc; + doc.Parse(thumbURL.GetData()); + const TiXmlNode* thumb = doc.FirstChild("thumb"); + while (thumb) + { + artist->InsertEndChild(*thumb); + thumb = thumb->NextSibling("thumb"); + } + } + XMLUtils::SetString(artist, "path", strPath); + + // Discography + for (const auto& it : discography) + { + // add a <album> tag + TiXmlElement discoElement("album"); + TiXmlNode* node = artist->InsertEndChild(discoElement); + XMLUtils::SetString(node, "title", it.strAlbum); + XMLUtils::SetString(node, "year", it.strYear); + XMLUtils::SetString(node, "musicbrainzreleasegroupid", it.strReleaseGroupMBID); + } + + return true; +} + +void CArtist::SetDateAdded(const std::string& strDateAdded) +{ + dateAdded.SetFromDBDateTime(strDateAdded); +} + +void CArtist::SetDateUpdated(const std::string& strDateUpdated) +{ + dateUpdated.SetFromDBDateTime(strDateUpdated); +} + +void CArtist::SetDateNew(const std::string& strDateNew) +{ + dateNew.SetFromDBDateTime(strDateNew); +} + diff --git a/xbmc/music/Artist.h b/xbmc/music/Artist.h new file mode 100644 index 0000000..2ec77a8 --- /dev/null +++ b/xbmc/music/Artist.h @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "utils/ScraperUrl.h" +#include "utils/StringUtils.h" + +#include <map> +#include <string> +#include <utility> +#include <vector> + +class TiXmlNode; +class CAlbum; +class CMusicDatabase; + +class CDiscoAlbum +{ +public: + std::string strAlbum; + std::string strYear; + std::string strReleaseGroupMBID; +}; + +class CArtist +{ +public: + int idArtist = -1; + bool operator<(const CArtist& a) const + { + if (strMusicBrainzArtistID.empty() && a.strMusicBrainzArtistID.empty()) + { + if (strArtist < a.strArtist) return true; + if (strArtist > a.strArtist) return false; + return false; + } + + if (strMusicBrainzArtistID < a.strMusicBrainzArtistID) return true; + if (strMusicBrainzArtistID > a.strMusicBrainzArtistID) return false; + return false; + } + + void MergeScrapedArtist(const CArtist& source, bool override = true); + + void Reset() + { + strArtist.clear(); + strSortName.clear(); + strType.clear(); + strGender.clear(); + strDisambiguation.clear(); + genre.clear(); + strBiography.clear(); + styles.clear(); + moods.clear(); + instruments.clear(); + strBorn.clear(); + strFormed.clear(); + strDied.clear(); + strDisbanded.clear(); + yearsActive.clear(); + thumbURL.Clear(); + art.clear(); + discography.clear(); + idArtist = -1; + strPath.clear(); + dateAdded.Reset(); + dateUpdated.Reset(); + dateNew.Reset(); + bScrapedMBID = false; + strLastScraped.clear(); + } + + /*! \brief Load artist information from an XML file. + See CVideoInfoTag::Load for a description of the types of elements we load. + \param element the root XML element to parse. + \param append whether information should be added to the existing tag, or whether it should be reset first. + \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false. + \sa CVideoInfoTag::Load + */ + bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false); + bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath); + + void SetDateAdded(const std::string& strDateAdded); + void SetDateUpdated(const std::string& strDateUpdated); + void SetDateNew(const std::string& strDateNew); + + std::string strArtist; + std::string strSortName; + std::string strMusicBrainzArtistID; + std::string strType; + std::string strGender; + std::string strDisambiguation; + std::vector<std::string> genre; + std::string strBiography; + std::vector<std::string> styles; + std::vector<std::string> moods; + std::vector<std::string> instruments; + std::string strBorn; + std::string strFormed; + std::string strDied; + std::string strDisbanded; + std::vector<std::string> yearsActive; + std::string strPath; + CScraperUrl thumbURL; // Data for available remote art + std::map<std::string, std::string> art; // Current artwork - thumb, fanart etc. + std::vector<CDiscoAlbum> discography; + CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned + CDateTime dateUpdated; // Time db record Last modified + CDateTime dateNew; // Time db record created + bool bScrapedMBID = false; + std::string strLastScraped; +}; + +class CArtistCredit +{ + friend class CAlbum; + friend class CMusicDatabase; + +public: + CArtistCredit() = default; + explicit CArtistCredit(std::string strArtist) : m_strArtist(std::move(strArtist)) {} + CArtistCredit(std::string strArtist, std::string strMusicBrainzArtistID) + : m_strArtist(std::move(strArtist)), m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID)) + { + } + CArtistCredit(std::string strArtist, std::string strSortName, std::string strMusicBrainzArtistID) + : m_strArtist(std::move(strArtist)), + m_strSortName(std::move(strSortName)), + m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID)) + { + } + + bool operator<(const CArtistCredit& a) const + { + if (m_strMusicBrainzArtistID.empty() && a.m_strMusicBrainzArtistID.empty()) + { + if (m_strArtist < a.m_strArtist) return true; + if (m_strArtist > a.m_strArtist) return false; + return false; + } + + if (m_strMusicBrainzArtistID < a.m_strMusicBrainzArtistID) return true; + if (m_strMusicBrainzArtistID > a.m_strMusicBrainzArtistID) return false; + return false; + } + + std::string GetArtist() const { return m_strArtist; } + std::string GetSortName() const { return m_strSortName; } + std::string GetMusicBrainzArtistID() const { return m_strMusicBrainzArtistID; } + int GetArtistId() const { return idArtist; } + bool HasScrapedMBID() const { return m_bScrapedMBID; } + void SetArtist(const std::string &strArtist) { m_strArtist = strArtist; } + void SetSortName(const std::string &strSortName) { m_strSortName = strSortName; } + void SetMusicBrainzArtistID(const std::string &strMusicBrainzArtistID) { m_strMusicBrainzArtistID = strMusicBrainzArtistID; } + void SetArtistId(int idArtist) { this->idArtist = idArtist; } + void SetScrapedMBID(bool scrapedMBID) { this->m_bScrapedMBID = scrapedMBID; } + +private: + int idArtist = -1; + std::string m_strArtist; + std::string m_strSortName; + std::string m_strMusicBrainzArtistID; + bool m_bScrapedMBID = false; // Flag that mbid is from album merge of scarper results not derived from tags +}; + +typedef std::vector<CArtist> VECARTISTS; +typedef std::vector<CArtistCredit> VECARTISTCREDITS; + +const std::string BLANKARTIST_FAKEMUSICBRAINZID = "Artist Tag Missing"; +const std::string BLANKARTIST_NAME = "[Missing Tag]"; +const int BLANKARTIST_ID = 1; +const std::string VARIOUSARTISTS_MBID = "89ad4ac3-39f7-470e-963a-56509c546377"; + +#define ROLE_ARTIST 1 //Default role + +class CMusicRole +{ +public: + CMusicRole() = default; + CMusicRole(std::string strRole, std::string strArtist) + : idRole(-1), m_strRole(std::move(strRole)), m_strArtist(std::move(strArtist)), idArtist(-1) + { + } + CMusicRole(int role, std::string strRole, std::string strArtist, int ArtistId) + : idRole(role), + m_strRole(std::move(strRole)), + m_strArtist(std::move(strArtist)), + idArtist(ArtistId) + { + } + std::string GetArtist() const { return m_strArtist; } + std::string GetRoleDesc() const { return m_strRole; } + int GetRoleId() const { return idRole; } + int GetArtistId() const { return idArtist; } + void SetArtistId(int iArtistId) { idArtist = iArtistId; } + + bool operator==(const CMusicRole& a) const + { + if (StringUtils::EqualsNoCase(m_strRole, a.m_strRole)) + return StringUtils::EqualsNoCase(m_strArtist, a.m_strArtist); + else + return false; + } +private: + int idRole; + std::string m_strRole; + std::string m_strArtist; + int idArtist; +}; + +typedef std::vector<CMusicRole> VECMUSICROLES; + + + diff --git a/xbmc/music/CMakeLists.txt b/xbmc/music/CMakeLists.txt new file mode 100644 index 0000000..5e1fd42 --- /dev/null +++ b/xbmc/music/CMakeLists.txt @@ -0,0 +1,25 @@ +set(SOURCES Album.cpp + Artist.cpp + ContextMenus.cpp + GUIViewStateMusic.cpp + MusicDatabase.cpp + MusicDbUrl.cpp + MusicInfoLoader.cpp + MusicLibraryQueue.cpp + MusicThumbLoader.cpp + MusicUtils.cpp + Song.cpp) + +set(HEADERS Album.h + Artist.h + ContextMenus.h + GUIViewStateMusic.h + MusicDatabase.h + MusicDbUrl.h + MusicInfoLoader.h + MusicLibraryQueue.h + MusicThumbLoader.h + MusicUtils.h + Song.h) + +core_add_library(music) diff --git a/xbmc/music/ContextMenus.cpp b/xbmc/music/ContextMenus.cpp new file mode 100644 index 0000000..bf7dd47 --- /dev/null +++ b/xbmc/music/ContextMenus.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ContextMenus.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "music/MusicUtils.h" +#include "music/dialogs/GUIDialogMusicInfo.h" +#include "tags/MusicInfoTag.h" + +#include <utility> + +using namespace CONTEXTMENU; + +CMusicInfo::CMusicInfo(MediaType mediaType) + : CStaticContextMenuAction(19033), m_mediaType(std::move(mediaType)) +{ +} + +bool CMusicInfo::IsVisible(const CFileItem& item) const +{ + return (item.HasMusicInfoTag() && item.GetMusicInfoTag()->GetType() == m_mediaType) || + (m_mediaType == MediaTypeArtist && item.IsVideoDb() && item.HasProperty("artist_musicid")) || + (m_mediaType == MediaTypeAlbum && item.IsVideoDb() && item.HasProperty("album_musicid")); +} + +bool CMusicInfo::Execute(const std::shared_ptr<CFileItem>& item) const +{ + CGUIDialogMusicInfo::ShowFor(item.get()); + return true; +} + +bool CMusicBrowse::IsVisible(const CFileItem& item) const +{ + if (item.IsFileFolder(EFILEFOLDER_MASK_ONBROWSE)) + return false; // handled by CMediaWindow + + return item.m_bIsFolder && MUSIC_UTILS::IsItemPlayable(item); +} + +bool CMusicBrowse::Execute(const std::shared_ptr<CFileItem>& item) const +{ + auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + if (windowMgr.GetActiveWindow() == WINDOW_MUSIC_NAV) + { + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, WINDOW_MUSIC_NAV, 0, GUI_MSG_UPDATE); + msg.SetStringParam(item->GetPath()); + windowMgr.SendMessage(msg); + } + else + { + windowMgr.ActivateWindow(WINDOW_MUSIC_NAV, {item->GetPath(), "return"}); + } + return true; +} + +bool CMusicPlay::IsVisible(const CFileItem& item) const +{ + return MUSIC_UTILS::IsItemPlayable(item); +} + +bool CMusicPlay::Execute(const std::shared_ptr<CFileItem>& item) const +{ + MUSIC_UTILS::PlayItem(item); + return true; +} + +bool CMusicPlayNext::IsVisible(const CFileItem& item) const +{ + if (!item.CanQueue()) + return false; + + return MUSIC_UTILS::IsItemPlayable(item); +} + +bool CMusicPlayNext::Execute(const std::shared_ptr<CFileItem>& item) const +{ + MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_BEGIN); + return true; +} + +bool CMusicQueue::IsVisible(const CFileItem& item) const +{ + if (!item.CanQueue()) + return false; + + return MUSIC_UTILS::IsItemPlayable(item); +} + +namespace +{ +void SelectNextItem(int windowID) +{ + auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + CGUIWindow* window = windowMgr.GetWindow(windowID); + if (window) + { + const int viewContainerID = window->GetViewContainerID(); + if (viewContainerID > 0) + { + CGUIMessage msg1(GUI_MSG_ITEM_SELECTED, windowID, viewContainerID); + windowMgr.SendMessage(msg1, windowID); + + CGUIMessage msg2(GUI_MSG_ITEM_SELECT, windowID, viewContainerID, msg1.GetParam1() + 1); + windowMgr.SendMessage(msg2, windowID); + } + } +} +} // unnamed namespace + +bool CMusicQueue::Execute(const std::shared_ptr<CFileItem>& item) const +{ + MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_END); + + // Set selection to next item in active window's view. + const int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + SelectNextItem(windowID); + + return true; +} diff --git a/xbmc/music/ContextMenus.h b/xbmc/music/ContextMenus.h new file mode 100644 index 0000000..7327592 --- /dev/null +++ b/xbmc/music/ContextMenus.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ContextMenuItem.h" +#include "media/MediaType.h" + +#include <memory> + +class CFileItem; + +namespace CONTEXTMENU +{ + +struct CMusicInfo : CStaticContextMenuAction +{ + explicit CMusicInfo(MediaType mediaType); + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; + +private: + const MediaType m_mediaType; +}; + +struct CAlbumInfo : CMusicInfo +{ + CAlbumInfo() : CMusicInfo(MediaTypeAlbum) {} +}; + +struct CArtistInfo : CMusicInfo +{ + CArtistInfo() : CMusicInfo(MediaTypeArtist) {} +}; + +struct CSongInfo : CMusicInfo +{ + CSongInfo() : CMusicInfo(MediaTypeSong) {} +}; + +struct CMusicBrowse : CStaticContextMenuAction +{ + CMusicBrowse() : CStaticContextMenuAction(37015) {} // Browse into + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CMusicPlay : CStaticContextMenuAction +{ + CMusicPlay() : CStaticContextMenuAction(208) {} // Play + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CMusicPlayNext : CStaticContextMenuAction +{ + CMusicPlayNext() : CStaticContextMenuAction(10008) {} // Play next + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CMusicQueue : CStaticContextMenuAction +{ + CMusicQueue() : CStaticContextMenuAction(13347) {} // Queue item + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +} // namespace CONTEXTMENU diff --git a/xbmc/music/GUIViewStateMusic.cpp b/xbmc/music/GUIViewStateMusic.cpp new file mode 100644 index 0000000..f14f501 --- /dev/null +++ b/xbmc/music/GUIViewStateMusic.cpp @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIViewStateMusic.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "filesystem/Directory.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "playlists/PlayListTypes.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileExtensionProvider.h" +#include "utils/SortUtils.h" +#include "utils/log.h" +#include "view/ViewStateSettings.h" + +using namespace XFILE; +using namespace MUSICDATABASEDIRECTORY; + +PLAYLIST::Id CGUIViewStateWindowMusic::GetPlaylist() const +{ + return PLAYLIST::TYPE_MUSIC; +} + +bool CGUIViewStateWindowMusic::AutoPlayNextItem() +{ + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + return settings->GetBool(CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM) && + !settings->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT); +} + +std::string CGUIViewStateWindowMusic::GetLockType() +{ + return "music"; +} + +std::string CGUIViewStateWindowMusic::GetExtensions() +{ + return CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(); +} + +VECSOURCES& CGUIViewStateWindowMusic::GetSources() +{ + return CGUIViewState::GetSources(); +} + +CGUIViewStateMusicSearch::CGUIViewStateMusicSearch(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + SortAttribute sortAttribute = SortAttributeNone; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttribute = SortAttributeIgnoreArticle; + + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D", "%L", "%A")); // Title - Artist, Duration | Label, Artist + SetSortMethod(SortByTitle); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + + LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateMusicSearch::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs")); +} + +CGUIViewStateMusicDatabase::CGUIViewStateMusicDatabase(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + CMusicDatabaseDirectory dir; + NODE_TYPE NodeType=dir.GetDirectoryChildType(items.GetPath()); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT); + if (strTrack.empty()) + strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat; + if (strAlbum.empty()) + strAlbum = "%B"; // album + CLog::Log(LOGDEBUG, "Custom album format = [{}]", strAlbum); + SortAttribute sortAttribute = SortAttributeNone; + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttribute = SortAttributeIgnoreArticle; + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName); + + switch (NodeType) + { + case NODE_TYPE_OVERVIEW: + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_TOP100: + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_GENRE: + { + AddSortMethod(SortByGenre, 515, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty + SetSortMethod(SortByGenre); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderAscending); + } + break; + case NODE_TYPE_ROLE: + { + AddSortMethod(SortByNone, 576, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty + SetSortMethod(SortByPlaycount); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_YEAR: + { + AddSortMethod(SortByLabel, 562, LABEL_MASKS("%F", "", "%Y", "")); // Filename, empty | Year, empty + SetSortMethod(SortByLabel); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderAscending); + } + break; + case NODE_TYPE_ARTIST: + { + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", "%A", "")); // Filename, empty | Artist, empty + AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", "%A", "%a")); // Filename, empty | Artist, dateAdded + SetSortMethod(SortByArtist); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavartists"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_ALBUM: + { + // album + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist + // artist + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist + // artist / year + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year + // discs + AddSortMethod( + SortByTotalDiscs, sortAttribute, 38077, + LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs + // year + AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y")); // Filename, empty | Userdefined, Year + // original release year + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + AddSortMethod( + SortByOrigDate, sortAttribute, 38079, + LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date + // album date added + AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded + // play count + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count + // last played + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played + // rating + AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating + // userrating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavalbums"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + { + AddSortMethod(SortByNone, 552, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded + SetSortMethod(SortByNone); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + { + AddSortMethod(SortByNone, 552, LABEL_MASKS(strTrack, "%a")); // Userdefined, dateAdded | empty, empty + SetSortMethod(SortByNone); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + { + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode); + } + break; + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + { + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode); + } + break; + case NODE_TYPE_ALBUM_TOP100: + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_SINGLES: + { + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D")); + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year + // original release date (singles can be re-released) + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + AddSortMethod(SortByOrigDate, 38079, + LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date + AddSortMethod(SortByDateAdded, 570, + LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_ALBUM_TOP100_SONGS: + case NODE_TYPE_SONG: + { + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D")); + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year + // original release date + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + AddSortMethod(SortByOrigDate, 38079, + LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty + AddSortMethod(SortByBPM, 38080, LABEL_MASKS(strTrack, "%f")); // Userdefined, bpm, empty,empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs"); + // the "All Albums" entries always default to SortByAlbum as this is most logical - user can always + // change it and the change will be saved for this particular path + if (dir.IsAllItem(items.GetPath())) + SetSortMethod(SortByAlbum); + else + SetSortMethod(viewState->m_sortDescription); + + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_SONG_TOP100: + { + AddSortMethod(SortByNone, 576, LABEL_MASKS("%T - %A", "%V")); + SetSortMethod(SortByPlaycount); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_DISC: + { + AddSortMethod(SortByNone, 427, LABEL_MASKS("%L")); // Use the existing label + SetSortMethod(SortByNone); + } + break; + default: + break; + } + + LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateMusicDatabase::SaveViewState() +{ + CMusicDatabaseDirectory dir; + NODE_TYPE NodeType=dir.GetDirectoryChildType(m_items.GetPath()); + + switch (NodeType) + { + case NODE_TYPE_ARTIST: + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavartists")); + break; + case NODE_TYPE_ALBUM: + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavalbums")); + break; + case NODE_TYPE_SINGLES: + case NODE_TYPE_SONG: + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs")); + break; + default: + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV); + break; + } +} + +CGUIViewStateMusicSmartPlaylist::CGUIViewStateMusicSmartPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + SortAttribute sortAttribute = SortAttributeNone; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttribute = SortAttributeIgnoreArticle; + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName); + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs"); + + if (items.GetContent() == "songs" || items.GetContent() == "mixed") + { + std::string strTrack=settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D")); + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title, Artist, Rating| empty, empty + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + AddSortMethod(SortByOrigDate, 38079, + LABEL_MASKS("%T - %A", "%e")); // Title - Artist, original date, empty, empty + AddSortMethod(SortByBPM, 38080, + LABEL_MASKS("%T - %A", "%f")); // Title - Artist, bpm, empty, empty + + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS(strTrack, "%D")); + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode); + } + else if (items.GetContent() == "albums") + { + std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat; + if (strAlbum.empty()) + strAlbum = "%B"; // album + // album + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist + // artist + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist + // artist / year + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year + // discs + AddSortMethod( + SortByTotalDiscs, sortAttribute, 38077, + LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs + // year + AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y")); + // original release date + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + AddSortMethod( + SortByOrigDate, 38079, + LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date + // album date added + AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded + // play count + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count + // last played + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played + // rating + AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating + // userrating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating + + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS("%F", "", strAlbum, "%D")); + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode); + } + else + { + CLog::Log(LOGERROR,"Music Smart Playlist must be one of songs, mixed or albums"); + } + + LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateMusicSmartPlaylist::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs")); +} + +CGUIViewStateMusicPlaylist::CGUIViewStateMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + SortAttribute sortAttribute = SortAttributeNone; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttribute = SortAttributeIgnoreArticle; + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName); + + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrack, "%D")); + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D")); + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title - Artist, Duration| empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating| empty, empty + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + SetSortMethod(SortByPlaylistOrder); + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicfiles"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + + LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateMusicPlaylist::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV); +} + +CGUIViewStateWindowMusicNav::CGUIViewStateWindowMusicNav(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + SortAttribute sortAttribute = SortAttributeNone; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttribute = SortAttributeIgnoreArticle; + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName); + + if (items.IsVirtualDirectoryRoot()) + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Foldername, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + else if (items.GetPath() == "special://musicplaylists/") + { // playlists list sorts by label only, ignoring folders + AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, + LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty + SetSortMethod(SortByLabel); + } + else + { + if (items.IsVideoDb() && items.Size() > (settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS)?1:0)) + { + XFILE::VIDEODATABASEDIRECTORY::CQueryParams params; + XFILE::CVideoDatabaseDirectory::GetQueryParams(items[settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS) ? 1 : 0]->GetPath(), params); + if (params.GetMVideoId() != -1) + { + AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS("%T", "%Y")); // Filename, Duration | Foldername, empty + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y")); + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%Y")); + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T", "%Y")); + + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty + } + else + { + AddSortMethod(SortByLabel, 551, LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty + SetSortMethod(SortByLabel); + } + } + else + { + //In navigation of music files tag data is scanned whenever present and can be used as sort criteria + //hence sort methods available are similar to song node (not the same as only tag data) + //Unfortunately anything here appears at all levels of file navigation even if no song files there. + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT); + if (strTrack.empty()) + strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByLabel, 551, LABEL_MASKS(strTrack, "%D", "%L", ""), // Userdefined, Duration | FolderName, empty + settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + AddSortMethod(SortBySize, 553, LABEL_MASKS("%F", "%I", "%L", "%I")); // Filename, Size | Foldername, Size + AddSortMethod(SortByDate, 552, LABEL_MASKS("%F", "%J", "%L", "%J")); // Filename, Date | Foldername, Date + AddSortMethod(SortByFile, 561, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty + AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty + AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty + AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist(year), Title, Year| empty, empty + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year + + SetSortMethod(SortByLabel); + } + const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + + SetSortOrder(SortOrderAscending); + } + LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateWindowMusicNav::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV); +} + +void CGUIViewStateWindowMusicNav::AddOnlineShares() +{ + if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVirtualShares) + return; + + VECSOURCES *musicSources = CMediaSourceSettings::GetInstance().GetSources("music"); + + for (int i = 0; i < (int)musicSources->size(); ++i) + { + CMediaSource share = musicSources->at(i); + } +} + +VECSOURCES& CGUIViewStateWindowMusicNav::GetSources() +{ + // Setup shares we want to have + m_sources.clear(); + CFileItemList items; + + CDirectory::GetDirectory("library://music/", items, "", DIR_FLAG_DEFAULTS); + for (int i=0; i<items.Size(); ++i) + { + CFileItemPtr item=items[i]; + CMediaSource share; + share.strName = item->GetLabel(); + share.strPath = item->GetPath(); + share.m_strThumbnailImage = item->GetArt("icon"); + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + m_sources.push_back(share); + } + + AddOnlineShares(); + + return CGUIViewStateWindowMusic::GetSources(); +} + +CGUIViewStateWindowMusicPlaylist::CGUIViewStateWindowMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items) +{ + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT); + if (strTrack.empty()) + strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + + AddSortMethod(SortByNone, 551, LABEL_MASKS(strTrack, "%D", "%L", "")); // Userdefined, Duration | FolderName, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + + LoadViewState(items.GetPath(), WINDOW_MUSIC_PLAYLIST); +} + +void CGUIViewStateWindowMusicPlaylist::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_PLAYLIST); +} + +PLAYLIST::Id CGUIViewStateWindowMusicPlaylist::GetPlaylist() const +{ + return PLAYLIST::TYPE_MUSIC; +} + +bool CGUIViewStateWindowMusicPlaylist::AutoPlayNextItem() +{ + return false; +} + +bool CGUIViewStateWindowMusicPlaylist::HideParentDirItems() +{ + return true; +} + +VECSOURCES& CGUIViewStateWindowMusicPlaylist::GetSources() +{ + m_sources.clear(); + // Playlist share + CMediaSource share; + share.strPath = "playlistmusic://"; + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + m_sources.push_back(share); + + // CGUIViewState::GetSources would add music plugins + return m_sources; +} diff --git a/xbmc/music/GUIViewStateMusic.h b/xbmc/music/GUIViewStateMusic.h new file mode 100644 index 0000000..7f7311a --- /dev/null +++ b/xbmc/music/GUIViewStateMusic.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "view/GUIViewState.h" + +class CGUIViewStateWindowMusic : public CGUIViewState +{ +public: + explicit CGUIViewStateWindowMusic(const CFileItemList& items) : CGUIViewState(items) {} +protected: + VECSOURCES& GetSources() override; + PLAYLIST::Id GetPlaylist() const override; + bool AutoPlayNextItem() override; + std::string GetLockType() override; + std::string GetExtensions() override; +}; + +class CGUIViewStateMusicSearch : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateMusicSearch(const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateMusicDatabase : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateMusicDatabase(const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateMusicSmartPlaylist : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateMusicSmartPlaylist(const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateMusicPlaylist : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateMusicPlaylist(const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateWindowMusicNav : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateWindowMusicNav(const CFileItemList& items); + +protected: + void SaveViewState() override; + VECSOURCES& GetSources() override; + +private: + void AddOnlineShares(); +}; + +class CGUIViewStateWindowMusicPlaylist : public CGUIViewStateWindowMusic +{ +public: + explicit CGUIViewStateWindowMusicPlaylist(const CFileItemList& items); + +protected: + void SaveViewState() override; + PLAYLIST::Id GetPlaylist() const override; + bool AutoPlayNextItem() override; + bool HideParentDirItems() override; + VECSOURCES& GetSources() override; +}; diff --git a/xbmc/music/MusicDatabase.cpp b/xbmc/music/MusicDatabase.cpp new file mode 100644 index 0000000..91ff208 --- /dev/null +++ b/xbmc/music/MusicDatabase.cpp @@ -0,0 +1,13822 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicDatabase.h" + +#include "Album.h" +#include "Artist.h" +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "LangInfo.h" +#include "ServiceBroker.h" +#include "Song.h" +#include "TextureCache.h" +#include "URL.h" +#include "Util.h" +#include "addons/Addon.h" +#include "addons/AddonManager.h" +#include "addons/AddonSystemSettings.h" +#include "addons/Scraper.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h" +#include "dbwrappers/dataset.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "events/EventLog.h" +#include "events/NotificationEvent.h" +#include "filesystem/Directory.h" +#include "filesystem/DirectoryCache.h" +#include "filesystem/File.h" +#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/helpers/DialogHelper.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDbUrl.h" +#include "music/MusicLibraryQueue.h" +#include "music/tags/MusicInfoTag.h" +#include "network/Network.h" +#include "network/cddb.h" +#include "playlists/SmartPlayList.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/LegacyPathTranslation.h" +#include "utils/MathUtils.h" +#include "utils/Random.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <inttypes.h> + +using namespace XFILE; +using namespace MUSICDATABASEDIRECTORY; +using namespace KODI::MESSAGING; +using namespace MUSIC_INFO; + +using ADDON::AddonPtr; +using KODI::MESSAGING::HELPERS::DialogResponse; + +#define RECENTLY_PLAYED_LIMIT 25 +#define MIN_FULL_SEARCH_LENGTH 3 + +#ifdef HAS_DVD_DRIVE +using namespace CDDB; +using namespace MEDIA_DETECT; +#endif + +static void AnnounceRemove(const std::string& content, int id) +{ + CVariant data; + data["type"] = content; + data["id"] = id; + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + data["transaction"] = true; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data); +} + +static void AnnounceUpdate(const std::string& content, int id, bool added = false) +{ + CVariant data; + data["type"] = content; + data["id"] = id; + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + data["transaction"] = true; + if (added) + data["added"] = true; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data); +} + +CMusicDatabase::CMusicDatabase(void) +{ + m_translateBlankArtist = true; +} + +CMusicDatabase::~CMusicDatabase(void) +{ + EmptyCache(); +} + +bool CMusicDatabase::Open() +{ + return CDatabase::Open( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic); +} + +void CMusicDatabase::CreateTables() +{ + CLog::Log(LOGINFO, "create artist table"); + m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, " + " strArtist varchar(256), strMusicBrainzArtistID text, " + " strSortName text, " + " strType text, strGender text, strDisambiguation text, " + " strBorn text, strFormed text, strGenres text, strMoods text, " + " strStyles text, strInstruments text, strBiography text, " + " strDied text, strDisbanded text, strYearsActive text, " + " strImage text, " + " lastScraped varchar(20) default NULL, " + " bScrapedMBID INTEGER NOT NULL DEFAULT 0, " + " idInfoSetting INTEGER NOT NULL DEFAULT 0, " + " dateAdded TEXT, dateNew TEXT, dateModified TEXT)"); + // Create missing artist tag artist [Missing]. + std::string strSQL = + PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) " + "VALUES( %i, '%s', '%s', '%s' )", + BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_NAME.c_str(), + BLANKARTIST_FAKEMUSICBRAINZID.c_str()); + m_pDS->exec(strSQL); + + CLog::Log(LOGINFO, "create album table"); + m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, " + " strAlbum varchar(256), strMusicBrainzAlbumID text, " + " strReleaseGroupMBID text, " + " strArtistDisp text, strArtistSort text, strGenres text, " + " strReleaseDate TEXT, strOrigReleaseDate TEXT, " + " bBoxedSet INTEGER NOT NULL DEFAULT 0, " + " bCompilation integer not null default '0', " + " strMoods text, strStyles text, strThemes text, " + " strReview text, strImage text, strLabel text, " + " strType text, " + " strReleaseStatus TEXT, " + " fRating FLOAT NOT NULL DEFAULT 0, " + " iVotes INTEGER NOT NULL DEFAULT 0, " + " iUserrating INTEGER NOT NULL DEFAULT 0, " + " lastScraped varchar(20) default NULL, " + " bScrapedMBID INTEGER NOT NULL DEFAULT 0, " + " strReleaseType text, " + " iDiscTotal INTEGER NOT NULL DEFAULT 0, " + " iAlbumDuration INTEGER NOT NULL DEFAULT 0, " + " idInfoSetting INTEGER NOT NULL DEFAULT 0, " + " dateAdded TEXT, dateNew TEXT, dateModified TEXT)"); + + CLog::Log(LOGINFO, "create audiobook table"); + m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, " + " strBook varchar(256), strAuthor text," + " bookmark integer, file text," + " dateAdded varchar (20) default NULL)"); + + CLog::Log(LOGINFO, "create album_artist table"); + m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, " + "strArtist text)"); + + CLog::Log(LOGINFO, "create album_source table"); + m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)"); + + CLog::Log(LOGINFO, "create genre table"); + m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))"); + + CLog::Log(LOGINFO, "create path table"); + m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)"); + + CLog::Log(LOGINFO, "create source table"); + m_pDS->exec( + "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)"); + + CLog::Log(LOGINFO, "create source_path table"); + m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))"); + + CLog::Log(LOGINFO, "create song table"); + m_pDS->exec("CREATE TABLE song (idSong integer primary key, " + " idAlbum integer, idPath integer, " + " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), " + " iTrack integer, iDuration integer, " + " strReleaseDate TEXT, strOrigReleaseDate TEXT, " + " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, " + " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, " + " lastplayed varchar(20) default NULL, " + " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, " + " userrating INTEGER NOT NULL DEFAULT 0, " + " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, " + " iBitRate INTEGER NOT NULL DEFAULT 0, " + " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, " + " strReplayGain text, " + " dateAdded TEXT, dateNew TEXT, dateModified TEXT)"); + CLog::Log(LOGINFO, "create song_artist table"); + m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder " + "integer, strArtist text)"); + CLog::Log(LOGINFO, "create song_genre table"); + m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)"); + + CLog::Log(LOGINFO, "create role table"); + m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)"); + m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role + + CLog::Log(LOGINFO, "create infosetting table"); + m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, " + "strScraperPath TEXT, strSettings TEXT)"); + + CLog::Log(LOGINFO, "create discography table"); + m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, " + "strReleaseGroupMBID TEXT)"); + + CLog::Log(LOGINFO, "create art table"); + m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, " + "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)"); + + CLog::Log(LOGINFO, "create versiontagscan table"); + m_pDS->exec("CREATE TABLE versiontagscan " + "(idVersion INTEGER, iNeedsScan INTEGER, " + "lastscanned VARCHAR(20), " + "lastcleaned VARCHAR(20), " + "artistlinksupdated VARCHAR(20), " + "genresupdated VARCHAR(20))"); + m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)", + GetSchemaVersion())); + + CLog::Log(LOGINFO, "create removed_link table"); + m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)"); +} + +void CMusicDatabase::CreateAnalytics() +{ + CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__); + m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))"); + m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)"); + m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))"); + m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)"); + + m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )"); + m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )"); + + m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))"); + + m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))"); + m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))"); + m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)"); + + m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))"); + + m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))"); + m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))"); + + m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)"); + + m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )"); + m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )"); + + m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))"); + m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)"); + m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)"); + m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)"); + m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )"); + //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set + m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )"); + + m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )"); + m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )"); + m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )"); + m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )"); + + m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )"); + m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )"); + + m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))"); + + m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )"); + + m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))"); + + CLog::Log(LOGINFO, "create triggers"); + m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN" + " DELETE FROM song WHERE song.idAlbum = old.idAlbum;" + " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;" + " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;" + " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';" + " END"); + m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN" + " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;" + " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;" + " DELETE FROM discography WHERE discography.idArtist = old.idArtist;" + " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';" + " END"); + m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN" + " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;" + " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;" + " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';" + " END"); + m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN" + " DELETE FROM source_path WHERE source_path.idSource = old.idSource;" + " DELETE FROM album_source WHERE album_source.idSource = old.idSource;" + " END"); + + /* Maintain date new and last modified for songs, albums and artists using triggers + MySQL triggers cannot modify a table that is already being used by the statement that invoked + the trigger (to avoid recursion), but can set NEW column values before insert or update. + Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table. + Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway. + // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real + */ + bool bisMySQL = StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql"); + + if (!bisMySQL) + { // SQLite trigger syntax - AFTER INSERT/UPDATE + m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN" + " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong" + " AND NEW.dateNew IS NULL;" + " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW" + " WHEN NEW.dateModified <= OLD.dateModified BEGIN" + " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;" + " END"); + m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN" + " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum" + " AND NEW.dateNew IS NULL;" + " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW" + " WHEN NEW.dateModified <= OLD.dateModified BEGIN" + " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;" + " END"); + m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN" + " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist" + " AND NEW.dateNew IS NULL;" + " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW" + " WHEN NEW.dateModified <= OLD.dateModified BEGIN" + " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;" + " END"); + + m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre" + " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');" + " END"); + } + else + { // MySQL trigger syntax - BEFORE INSERT/UPDATE + m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN" + " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;" + " SET NEW.dateModified = now();" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW" + " SET NEW.dateModified = now()"); + + m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN" + " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;" + " SET NEW.dateModified = now();" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW" + " SET NEW.dateModified = now()"); + + m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN" + " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;" + " SET NEW.dateModified = now();" + " END"); + m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW" + " SET NEW.dateModified = now()"); + + m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW" + " UPDATE versiontagscan SET genresupdated = now()"); + } + + // Triggers to maintain recent changes to album and song artist links in removed_link table + m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN " + "DELETE FROM removed_link " + "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; " + "END"); + m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN " + "DELETE FROM removed_link " + "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; " + "END"); + CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables + + // Create native functions stored in DB (MySQL/MariaDB only) + CreateNativeDBFunctions(); + + // we create views last to ensure all indexes are rolled in + CreateViews(); +} + +void CMusicDatabase::CreateRemovedLinkTriggers() +{ + // DELETE ON song_artist and album_artist tables need to be recreated after cleanup + m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN" + " INSERT INTO removed_link (idArtist, idMedia, idRole)" + " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);" + " END"); + m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN" + " INSERT INTO removed_link (idArtist, idMedia, idRole)" + " VALUES(OLD.idArtist, OLD.idAlbum, -1);" + " END"); +} + + +void CMusicDatabase::CreateViews() +{ + CLog::Log(LOGINFO, "create song view"); + m_pDS->exec("CREATE VIEW songview AS SELECT " + " song.idSong AS idSong, " + " song.strArtistDisp AS strArtists," + " song.strArtistSort AS strArtistSort," + " song.strGenres AS strGenres," + " strTitle, " + " iTrack, iDuration, " + " song.strReleaseDate as strReleaseDate, " + " song.strOrigReleaseDate as strOrigReleaseDate, " + " song.strDiscSubtitle as strDiscSubtitle, " + " strFileName, " + " strMusicBrainzTrackID, " + " iTimesPlayed, iStartOffset, iEndOffset, " + " lastplayed, " + " song.rating, " + " song.userrating, " + " song.votes, " + " comment, " + " song.idAlbum AS idAlbum, " + " strAlbum, " + " strPath, " + " album.strReleaseStatus as strReleaseStatus," + " album.bCompilation AS bCompilation," + " album.bBoxedSet AS bBoxedSet, " + " album.strArtistDisp AS strAlbumArtists," + " album.strArtistSort AS strAlbumArtistSort," + " album.strReleaseType AS strAlbumReleaseType," + " song.mood as mood," + " song.strReplayGain, " + " iBPM, " + " iBitRate, " + " iSampleRate, " + " iChannels, " + " album.iAlbumDuration AS iAlbumDuration, " + " album.iDiscTotal as iDiscTotal, " + " song.dateAdded as dateAdded, " + " song.dateNew AS dateNew, " + " song.dateModified AS dateModified " + "FROM song" + " JOIN album ON" + " song.idAlbum=album.idAlbum" + " JOIN path ON" + " song.idPath=path.idPath"); + + CLog::Log(LOGINFO, "create album view"); + m_pDS->exec("CREATE VIEW albumview AS SELECT " + "album.idAlbum AS idAlbum, " + "strAlbum, " + "strMusicBrainzAlbumID, " + "strReleaseGroupMBID, " + "album.strArtistDisp AS strArtists, " + "album.strArtistSort AS strArtistSort, " + "album.strGenres AS strGenres, " + "album.strReleaseDate as strReleaseDate, " + "album.strOrigReleaseDate as strOrigReleaseDate, " + "album.bBoxedSet AS bBoxedSet, " + "album.strMoods AS strMoods, " + "album.strStyles AS strStyles, " + "strThemes, " + "strReview, " + "strLabel, " + "strType, " + "strReleaseStatus, " + "album.strImage as strImage, " + "album.fRating, " + "album.iUserrating, " + "album.iVotes, " + "bCompilation, " + "bScrapedMBID," + "lastScraped," + "dateAdded, dateNew, dateModified, " + "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song " + "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, " + "strReleaseType, " + "iDiscTotal, " + "(SELECT MAX(song.lastplayed) FROM song " + "WHERE song.idAlbum = album.idAlbum) AS lastplayed, " + "iAlbumDuration " + "FROM album"); + + CLog::Log(LOGINFO, "create artist view"); + m_pDS->exec("CREATE VIEW artistview AS SELECT" + " idArtist, strArtist, strSortName, " + " strMusicBrainzArtistID, " + " strType, strGender, strDisambiguation, " + " strBorn, strFormed, strGenres," + " strMoods, strStyles, strInstruments, " + " strBiography, strDied, strDisbanded, " + " strYearsActive, strImage, " + " bScrapedMBID, lastScraped, " + " dateAdded, dateNew, dateModified " + "FROM artist"); + + CLog::Log(LOGINFO, "create albumartist view"); + m_pDS->exec("CREATE VIEW albumartistview AS SELECT" + " album_artist.idAlbum AS idAlbum, " + " album_artist.idArtist AS idArtist, " + " 0 AS idRole, " + " 'AlbumArtist' AS strRole, " + " artist.strArtist AS strArtist, " + " artist.strSortName AS strSortName," + " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, " + " album_artist.iOrder AS iOrder " + "FROM album_artist " + "JOIN artist ON " + " album_artist.idArtist = artist.idArtist"); + + CLog::Log(LOGINFO, "create songartist view"); + m_pDS->exec("CREATE VIEW songartistview AS SELECT" + " song_artist.idSong AS idSong, " + " song_artist.idArtist AS idArtist, " + " song_artist.idRole AS idRole, " + " role.strRole AS strRole, " + " artist.strArtist AS strArtist, " + " artist.strSortName AS strSortName," + " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, " + " song_artist.iOrder AS iOrder " + "FROM song_artist " + "JOIN artist ON " + " song_artist.idArtist = artist.idArtist " + "JOIN role ON " + " song_artist.idRole = role.idRole"); +} + +void CMusicDatabase::CreateNativeDBFunctions() +{ + // Create native functions in MySQL/MariaDB database only + if (!StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, + "mysql")) + return; + CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions"); + /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to + default utf8_general_ci collation that SQLite does via a collation sequence callback + function to StringUtils::AlphaNumericCompare + !@todo: the video needs these defined too for sorting in DB, then creation can be made common + */ + // clang-format off + // udfFirstNumberPos finds the position of the first digit in a string + m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos"); + m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n" + "RETURNS int \n" + "LANGUAGE SQL \n" + "DETERMINISTIC \n" + "NO SQL \n" + "SQL SECURITY INVOKER \n" + "BEGIN \n" + " DECLARE position int; \n" + " DECLARE tmppos int; \n" + " SET position = 5000; \n" + " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n" + " IF(position = 5000) THEN RETURN 0; END IF;\n" + " RETURN position; \n" + "END\n"); + + // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input + m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift"); + m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n" + "RETURNS varchar(1024)\n" + "LANGUAGE SQL\n" + "DETERMINISTIC\n" + "NO SQL\n" + "SQL SECURITY INVOKER\n" + "BEGIN\n" + " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n" + " DECLARE i int;\n" + " DECLARE symbolCharsLen int;\n" + " DECLARE symbol char(1);\n" + " SET sortString = instring;\n" + " SET i = 1;\n" + " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n" + " WHILE(i <= symbolCharsLen) DO\n" + " SET symbol = SUBSTRING(symbolChars, i, 1);\n" + " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n" + " SET i = i + 1;\n" + " END WHILE;\n" + " RETURN sortString;\n" + "END\n"); + + // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers + m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat"); + m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, " + "sameOrderChars char(25))\n" + "RETURNS varchar(1024)\n" + "LANGUAGE SQL\n" + "DETERMINISTIC\n" + "NO SQL\n" + "SQL SECURITY INVOKER\n" + "BEGIN\n" + " DECLARE sortString varchar(1024);\n" + " DECLARE shiftedString varchar(1024);\n" + " DECLARE inLength int;\n" + " DECLARE shiftedLength int;\n" + " DECLARE totalSympadLength int;\n" + " DECLARE symbolshifted512 varchar(1024);\n" + " DECLARE numStartIndex int; \n" + " DECLARE numEndIndex int; \n" + " DECLARE padLength int; \n" + " DECLARE totalPadLength int; \n" + " DECLARE i int; \n" + " DECLARE sameOrderCharsLen int;\n" + " SET totalPadLength = 0; \n" + " SET instring = TRIM(instring);\n" + " SET inLength = CHAR_LENGTH(inString);\n" + " SET sortString = instring; \n" + " SET numStartIndex = udfFirstNumberPos(instring); \n" + " SET numEndIndex = 0; \n" + " SET i = 1; \n" + " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n" + " WHILE(i <= sameOrderCharsLen) DO \n" + " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n" + " SET i = i + 1; \n" + " END WHILE; \n" + " WHILE(numStartIndex <> 0) DO \n" + " SET numStartIndex = numStartIndex + numEndIndex; \n" + " SET numEndIndex = numStartIndex; \n" + " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n" + " SET numEndIndex = numEndIndex + 1; \n" + " END WHILE; \n" + " SET numEndIndex = numEndIndex - 1; \n" + " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n" + " IF padLength < 0 THEN \n" + " SET padLength = 0; \n" + " END IF; \n" + " IF inLength + totalPadLength + padlength > 1024 THEN \n" + " -- Padding more digits would be too long, pad this one just enough \n" + " SET padLength = 1024 - inLength - totalPadLength; \n" + " SET numStartIndex = 0; \n" + " END IF; \n" + " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n" + " SET totalPadLength = totalPadLength + padLength; \n" + " IF numStartIndex <> 0 THEN \n" + " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n" + " END IF; \n" + " END WHILE; \n" + " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n" + " -- when there is space as this could double string length. Note '\\' needs escaping \n" + " SET numStartIndex = 1; \n" + " SET numEndIndex = inLength + totalPadLength; \n" + " IF numEndIndex < 1024 THEN \n" + " SET shiftedLength = 0; \n" + " SET totalSympadLength = 0; \n" + " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n" + " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n" + " SET numStartIndex = numStartIndex + 512; \n" + " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n" + " IF totalSympadLength = 0 THEN \n" + " SET shiftedString = symbolshifted512; \n" + " ELSE \n" + " IF totalSympadLength + shiftedLength > 1024 THEN \n" + " SET shiftedLength = 1024 - totalSympadLength; \n" + " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n" + " END IF; \n" + " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n" + " END IF; \n" + " SET totalSympadLength = totalSympadLength + shiftedLength; \n" + " END WHILE; \n" + " SET sortString = shiftedString; \n" + " END IF; \n" + " RETURN sortString; \n" + "END\n"); + // clang-format on +} + +void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath, + std::string& strPath, + std::string& strFileName) +{ + URIUtils::Split(strFileNameAndPath, strPath, strFileName); + // Keep protocol options as part of the path + if (URIUtils::IsURL(strFileNameAndPath)) + { + CURL url(strFileNameAndPath); + if (!url.GetProtocolOptions().empty()) + strPath += "|" + url.GetProtocolOptions(); + } +} + +bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource) +{ + BeginTransaction(); + SetLibraryLastUpdated(); + + album.idAlbum = AddAlbum(album.strAlbum, // + album.strMusicBrainzAlbumID, // + album.strReleaseGroupMBID, // + album.GetAlbumArtistString(), // + album.GetAlbumArtistSort(), // + album.GetGenreString(), // + album.strReleaseDate, // + album.strOrigReleaseDate, // + album.bBoxedSet, // + album.strLabel, // + album.strType, // + album.strReleaseStatus, // + album.bCompilation, // + album.releaseType); + + // Add the album artists + // Album must have at least one artist so set artist to [Missing] + if (album.artistCredits.empty()) + AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); + for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); + ++artistCredit) + { + artistCredit->idArtist = + AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), + artistCredit->GetSortName()); + AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(), + static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit))); + } + + // Add songs + for (auto song = album.songs.begin(); song != album.songs.end(); ++song) + { + song->idAlbum = album.idAlbum; + + song->idSong = AddSong(song->idSong, // + song->dateNew, // + song->idAlbum, // + song->strTitle, // + song->strMusicBrainzTrackID, // + song->strFileName, // + song->strComment, // + song->strMood, // + song->strThumb, // + song->GetArtistString(), // + song->GetArtistSort(), // + song->genre, // + song->iTrack, // + song->iDuration, // + song->strReleaseDate, // + song->strOrigReleaseDate, // + song->strDiscSubtitle, // + song->iTimesPlayed, // + song->iStartOffset, song->iEndOffset, // + song->lastPlayed, // + song->rating, // + song->userrating, // + song->votes, // + song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels, // + song->replayGain); + + // Song must have at least one artist so set artist to [Missing] + if (song->artistCredits.empty()) + AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); + + for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end(); + ++artistCredit) + { + artistCredit->idArtist = + AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), + artistCredit->GetSortName()); + AddSongArtist( + artistCredit->idArtist, song->idSong, ROLE_ARTIST, + artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet + static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit))); + } + // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID) + // and use COMPOSERSORT tag data to provide sort names for artists that are composers + AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort()); + } + + // Set album duration as total of all songs on album. + // Folder layout may mean AddAlbum call has added more songs to an existing album + std::string strSQL; + strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum); + int albumDuration = GetSingleValueInt(strSQL); + m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration, + album.idAlbum)); + + // Add album sources + if (idSource > 0) + AddAlbumSource(album.idAlbum, idSource); + else + { + // Use album path, or failing that song paths to determine sources for the album + AddAlbumSources(album.idAlbum, album.strPath); + } + + for (const auto& albumArt : album.art) + SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second); + + // Set album disc total + m_pDS->exec( + PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song " + "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i", + album.idAlbum)); + // Set a non-compilation album as a boxset if it has three or more distinct disc titles + if (!album.bBoxedSet && !album.bCompilation) + { + strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i", + album.idAlbum); + int numTitles = GetSingleValueInt(strSQL); + if (numTitles >= 3) + { + strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum); + m_pDS->exec(strSQL); + } + } + m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate " + "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i", + album.idAlbum)); + m_pDS->exec( + PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate " + "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i", + album.idAlbum)); + + std::string albumdateadded = + GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum)); + m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i", + albumdateadded.c_str(), album.idAlbum)); + + /* Update artist dateAdded values for artists involved in album as song or album artists. + Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is + derived from song dateadded values which are usually file dates (or the last scan). + It is used to indicate those artists with recent media. + For artists that are neither album nor song artists (other roles only) dateadded will be null. + */ + std::vector<std::string> artistIDs; + // Get distinct song and album artist IDs for this album + GetArtistsByAlbum(album.idAlbum, artistIDs); + std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")"; + strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' " + "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)", + albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str()); + m_pDS->exec(strSQL); + + CommitTransaction(); + return true; +} + +bool CMusicDatabase::UpdateAlbum(CAlbum& album) +{ + BeginTransaction(); + SetLibraryLastUpdated(); + + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + if (album.bBoxedSet) + { + bool isBoxset = IsAlbumBoxset(album.idAlbum); + if (!isBoxset) + { // not a boxset already so make sure we have enough discs & they all have titles + bool canBeBoxset = false; + std::string strSQL; + strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum); + int numDiscs = GetSingleValueInt(strSQL); + if (numDiscs >= 2) + { + canBeBoxset = true; + int discValue; + for (discValue = 1; discValue <= numDiscs; discValue++) + { + strSQL = + PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND " + "song.iTrack >> 16 = %i", + album.idAlbum, discValue); + std::string currentTitle = GetSingleValue(strSQL); + if (currentTitle.empty()) + { + currentTitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discValue); + strSQL = + PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND " + "song.iTrack >> 16 = %i", + currentTitle.c_str(), album.idAlbum, discValue); + ExecuteQuery(strSQL); + } + } + } + if (!canBeBoxset && album.bBoxedSet) + { + CLog::Log(LOGINFO, "{} : Album with id [{}] does not meet the requirements for a boxset.", + __FUNCTION__, album.idAlbum); + album.bBoxedSet = false; + } + } + } + UpdateAlbum(album.idAlbum, album.strAlbum, album.strMusicBrainzAlbumID, // + album.strReleaseGroupMBID, // + album.GetAlbumArtistString(), album.GetAlbumArtistSort(), // + album.GetGenreString(), // + StringUtils::Join(album.moods, itemSeparator), // + StringUtils::Join(album.styles, itemSeparator), // + StringUtils::Join(album.themes, itemSeparator), // + album.strReview, // + album.thumbURL.GetData(), // + album.strLabel, // + album.strType, // + album.strReleaseStatus, // + album.fRating, album.iUserrating, album.iVotes, // + album.strReleaseDate, // + album.strOrigReleaseDate, // + album.bBoxedSet, // + album.bCompilation, // + album.releaseType, // + album.bScrapedMBID); + + if (!album.bArtistSongMerge) + { + // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add + for (const auto& artistCredit : album.artistCredits) + UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID()); + } + else + { + // Replace the album artists with those scraped or set by JSON + DeleteAlbumArtistsByAlbum(album.idAlbum); + // Album must have at least one artist so set artist to [Missing] + if (album.artistCredits.empty()) + AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); + for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); + ++artistCredit) + { + artistCredit->idArtist = + AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), + artistCredit->GetSortName(), true); + AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(), + static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit))); + } + /* Replace the songs with those scraped or imported, but if new songs is empty + (such as when called from JSON) do not remove the original ones + Also updates nested data e.g. song artists, song genres and contributors + Do not check for artist link changes, that is done later for all songs and album + */ + int albumDuration = 0; + for (auto& song : album.songs) + { + UpdateSong(song); + albumDuration += song.iDuration; + } + if (albumDuration > 0) + m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i", + albumDuration, album.idAlbum)); + } + + if (!album.art.empty()) + SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art); + + CheckArtistLinksChanged(); + + CommitTransaction(); + return true; +} + +void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal) +{ + // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD + int iDate; + iDate = StringUtils::DateStringToYYYYMMDD(strRelease); + if (iDate < 0) + strRelease.clear(); + iDate = StringUtils::DateStringToYYYYMMDD(strOriginal); + if (iDate < 0) + strOriginal.clear(); + // Avoid missing release or original values unless both invalid or empty + if (!strRelease.empty() && strOriginal.empty()) + strOriginal = strRelease; + else if (strRelease.empty() && !strOriginal.empty()) + strRelease = strOriginal; +} + +int CMusicDatabase::AddSong(const int idSong, + const CDateTime& dtDateNew, + const int idAlbum, + const std::string& strTitle, + const std::string& strMusicBrainzTrackID, + const std::string& strPathAndFileName, + const std::string& strComment, + const std::string& strMood, + const std::string& strThumb, + const std::string& artistDisp, + const std::string& artistSort, + const std::vector<std::string>& genres, + int iTrack, + int iDuration, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + std::string& strDiscSubtitle, + const int iTimesPlayed, + int iStartOffset, + int iEndOffset, + const CDateTime& dtLastPlayed, + float rating, + int userrating, + int votes, + int iBPM, + int iBitRate, + int iSampleRate, + int iChannels, + const ReplayGain& replayGain) +{ + int idNew = -1; + std::string strSQL; + try + { + // We need at least the title + if (strTitle.empty()) + return -1; + + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strPath, strFileName; + SplitPath(strPathAndFileName, strPath, strFileName); + int idPath = AddPath(strPath); + + if (idSong <= 1) + { + if (!strMusicBrainzTrackID.empty()) + strSQL = PrepareSQL("SELECT idSong FROM song WHERE " + "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'", + idAlbum, iTrack, strMusicBrainzTrackID.c_str()); + else + strSQL = PrepareSQL("SELECT idSong FROM song WHERE " + "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i " + "AND strMusicBrainzTrackID IS NULL", + idAlbum, strFileName.c_str(), strTitle.c_str(), iTrack); + + if (!m_pDS->query(strSQL)) + return -1; + } + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + + // As all discs in a boxset have to have a title, generate one in the form of 'Disc N' + bool isBoxset = IsAlbumBoxset(idAlbum); + if (isBoxset && strDiscSubtitle.empty()) + { + int discno = iTrack >> 16; + strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discno); + } + + // Validate ISO8601 dates and ensure none missing + std::string strRelease = strReleaseDate; + std::string strOriginal = strOrigReleaseDate; + NormaliseSongDates(strRelease, strOriginal); + + // Get dateAdded from music file timestamp + std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName); + + strSQL = "INSERT INTO song (" + "idSong, dateNew, idAlbum, idPath, strArtistDisp, " + "strTitle, iTrack, iDuration, " + "strReleaseDate, strOrigReleaseDate, iBPM, " + "iBitrate, iSampleRate, iChannels, " + "strDiscSubtitle, strFileName, dateAdded, " + "strMusicBrainzTrackID, strArtistSort, " + "iTimesPlayed, iStartOffset, iEndOffset, " + "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) "; + + if (idSong <= 0) + // Song ID is autoincremented and dateNew set by trigger + strSQL += PrepareSQL("VALUES (NULL, NULL, "); + else + //Reuse song Id and original date when the Id added + strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str()); + + strSQL += + PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ", + idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration, + strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate, + iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str()); + + if (strMusicBrainzTrackID.empty()) + strSQL += PrepareSQL(",NULL"); + else + strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str()); + if (artistSort.empty() || artistSort.compare(artistDisp) == 0) + strSQL += PrepareSQL(",NULL"); + else + strSQL += PrepareSQL(",'%s'", artistSort.c_str()); + + if (dtLastPlayed.IsValid()) + strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", // + iTimesPlayed, iStartOffset, iEndOffset, + dtLastPlayed.GetAsDBDateTime().c_str(), // + static_cast<double>(rating), userrating, votes, // + strComment.c_str(), strMood.c_str(), replayGain.Get().c_str()); + else + strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", // + iTimesPlayed, iStartOffset, iEndOffset, // + static_cast<double>(rating), userrating, votes, // + strComment.c_str(), strMood.c_str(), replayGain.Get().c_str()); + m_pDS->exec(strSQL); + if (idSong <= 0) + idNew = (int)m_pDS->lastinsertid(); + else + idNew = idSong; + } + else + { + idNew = m_pDS->fv("idSong").get_asInt(); + m_pDS->close(); + UpdateSong(idNew, // + strTitle, // + strMusicBrainzTrackID, // + strPathAndFileName, // + strComment, // + strMood, // + strThumb, // + artistDisp, // + artistSort, // + genres, // + iTrack, // + iDuration, // + strReleaseDate, // + strOrigReleaseDate, // + strDiscSubtitle, // + iTimesPlayed, // + iStartOffset, iEndOffset, // + dtLastPlayed, // + rating, userrating, votes, // + replayGain, // + iBPM, iBitRate, iSampleRate, iChannels); + } + if (!strThumb.empty()) + SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb); + + // Song genres added, and genre string updated to use the standardised genre names + AddSongGenres(idNew, genres); + + AnnounceUpdate(MediaTypeSong, idNew, true); + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addsong ({})", strSQL); + } + return idNew; +} + +bool CMusicDatabase::GetSong(int idSong, CSong& song) +{ + try + { + song.Clear(); + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = + PrepareSQL("SELECT songview.*,songartistview.* FROM songview " + " JOIN songartistview ON songview.idSong = songartistview.idSong " + " WHERE songview.idSong = %i " + " ORDER BY songartistview.idRole, songartistview.iOrder", + idSong); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return false; + } + + int songArtistOffset = song_enumCount; + + song = GetSongFromDataset(m_pDS->get_sql_record()); + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt(); + if (idSongArtistRole == ROLE_ARTIST) + song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset)); + else + song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset)); + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong); + } + + return false; +} + +bool CMusicDatabase::UpdateSong(CSong& song, bool bArtists /*= true*/, bool bArtistLinks /*= true*/) +{ + int result = UpdateSong(song.idSong, + song.strTitle, // + song.strMusicBrainzTrackID, // + song.strFileName, // + song.strComment, // + song.strMood, // + song.strThumb, // + song.GetArtistString(), // + song.GetArtistSort(), // + song.genre, // + song.iTrack, // + song.iDuration, // + song.strReleaseDate, // + song.strOrigReleaseDate, // + song.strDiscSubtitle, // + song.iTimesPlayed, // + song.iStartOffset, song.iEndOffset, // + song.lastPlayed, // + song.rating, song.userrating, song.votes, // + song.replayGain, // + song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels); + if (result < 0) + return false; + + // Replace Song genres and update genre string using the standardised genre names + AddSongGenres(song.idSong, song.genre); + if (bArtists) + { + //Replace song artists and contributors + DeleteSongArtistsBySong(song.idSong); + // Song must have at least one artist so set artist to [Missing] + if (song.artistCredits.empty()) + AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); + for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end(); + ++artistCredit) + { + artistCredit->idArtist = + AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), + artistCredit->GetSortName()); + AddSongArtist(artistCredit->idArtist, song.idSong, ROLE_ARTIST, artistCredit->GetArtist(), + static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit))); + } + // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown) + // and use COMPOSERSORT tag data to provide sort names for artists that are composers + AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort()); + + if (bArtistLinks) + CheckArtistLinksChanged(); + } + + return true; +} + +int CMusicDatabase::UpdateSong(int idSong, + const std::string& strTitle, + const std::string& strMusicBrainzTrackID, + const std::string& strPathAndFileName, + const std::string& strComment, + const std::string& strMood, + const std::string& strThumb, + const std::string& artistDisp, + const std::string& artistSort, + const std::vector<std::string>& genres, + int iTrack, + int iDuration, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + const std::string& strDiscSubtitle, + int iTimesPlayed, + int iStartOffset, + int iEndOffset, + const CDateTime& dtLastPlayed, + float rating, + int userrating, + int votes, + const ReplayGain& replayGain, + int iBPM, + int iBitRate, + int iSampleRate, + int iChannels) +{ + if (idSong < 0) + return -1; + + std::string strSQL; + std::string strPath, strFileName; + SplitPath(strPathAndFileName, strPath, strFileName); + int idPath = AddPath(strPath); + + // Validate ISO8601 dates and ensure none missing + std::string strRelease = strReleaseDate; + std::string strOriginal = strOrigReleaseDate; + NormaliseSongDates(strRelease, strOriginal); + + std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName); + + strSQL = PrepareSQL( + "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', " + " strTitle = '%s', iTrack = %i, iDuration = %i, " + "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', " + "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, " + "dateAdded = '%s'", + idPath, artistDisp.c_str(), + StringUtils::Join( + genres, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator) + .c_str(), + strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(), + strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels, + strDateMedia.c_str()); + if (strMusicBrainzTrackID.empty()) + strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL"); + else + strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str()); + if (artistSort.empty() || artistSort.compare(artistDisp) == 0) + strSQL += PrepareSQL(", strArtistSort = NULL"); + else + strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str()); + + strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, " + "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ", + iStartOffset, iEndOffset, static_cast<double>(rating), userrating, votes, + strComment.c_str(), strMood.c_str(), replayGain.Get().c_str()); + + if (dtLastPlayed.IsValid()) + strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed, + dtLastPlayed.GetAsDBDateTime().c_str()); + else if (iTimesPlayed > 0) + strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed, + CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()); + else + strSQL += ", iTimesPlayed = 0, lastplayed = NULL "; + strSQL += PrepareSQL("WHERE idSong = %i", idSong); + + bool status = ExecuteQuery(strSQL); + + if (status) + AnnounceUpdate(MediaTypeSong, idSong); + return idSong; +} + +int CMusicDatabase::AddAlbum(const std::string& strAlbum, + const std::string& strMusicBrainzAlbumID, + const std::string& strReleaseGroupMBID, + const std::string& strArtist, + const std::string& strArtistSort, + const std::string& strGenre, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + bool bBoxedSet, + const std::string& strRecordLabel, + const std::string& strType, + const std::string& strReleaseStatus, + bool bCompilation, + CAlbum::ReleaseType releaseType) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + if (!strMusicBrainzAlbumID.empty()) + strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'", + strMusicBrainzAlbumID.c_str()); + else + strSQL = PrepareSQL("SELECT * FROM album " + "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' " + "AND strMusicBrainzAlbumID IS NULL", + strArtist.c_str(), strAlbum.c_str()); + m_pDS->query(strSQL); + std::string strCheckFlag = strType; + StringUtils::ToLower(strCheckFlag); + if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type + bBoxedSet = true; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // Does not exist, add it + strSQL = + PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, " + "strReleaseDate, strOrigReleaseDate, bBoxedSet, " + "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, " + "strMusicBrainzAlbumID, " + "strReleaseGroupMBID, strArtistSort) " + "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'", + strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), // + strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet, // + strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), // + bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str()); + + if (strMusicBrainzAlbumID.empty()) + strSQL += PrepareSQL(", NULL"); + else + strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str()); + if (strReleaseGroupMBID.empty()) + strSQL += PrepareSQL(", NULL"); + else + strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str()); + if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0) + strSQL += PrepareSQL(", NULL"); + else + strSQL += PrepareSQL(", '%s'", strArtistSort.c_str()); + strSQL += ")"; + m_pDS->exec(strSQL); + + return (int)m_pDS->lastinsertid(); + } + else + { + /* Exists in our database and being re-scanned from tags, so we should update it as the details + may have changed. + + Note that for multi-folder albums this will mean the last folder scanned will have the information + stored for it. Most values here should be the same across all songs anyway, but it does mean + that if there's any inconsistencies then only the last folders information will be taken. + + We make sure we clear out the link tables (album artists, album sources) and we reset + the last scraped time to make sure that online metadata is re-fetched. */ + int idAlbum = m_pDS->fv("idAlbum").get_asInt(); + m_pDS->close(); + + strSQL = "UPDATE album SET "; + if (!strMusicBrainzAlbumID.empty()) + strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", // + strAlbum.c_str(), strArtist.c_str()); + if (strReleaseGroupMBID.empty()) + strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,"); + else + strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str()); + if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0) + strSQL += PrepareSQL(" strArtistSort = NULL"); + else + strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str()); + + strSQL += + PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', " + "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', " + "bCompilation=%i, strReleaseType = '%s', " + "lastScraped = NULL " + "WHERE idAlbum=%i", + strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(), // + bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), + bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(), // + idAlbum); + m_pDS->exec(strSQL); + DeleteAlbumArtistsByAlbum(idAlbum); + DeleteAlbumSources(idAlbum); + return idAlbum; + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL); + } + + return -1; +} + +int CMusicDatabase::UpdateAlbum(int idAlbum, + const std::string& strAlbum, + const std::string& strMusicBrainzAlbumID, + const std::string& strReleaseGroupMBID, + const std::string& strArtist, + const std::string& strArtistSort, + const std::string& strGenre, + const std::string& strMoods, + const std::string& strStyles, + const std::string& strThemes, + const std::string& strReview, + const std::string& strImage, + const std::string& strLabel, + const std::string& strType, + const std::string& strReleaseStatus, + float fRating, + int iUserrating, + int iVotes, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + bool bBoxedSet, + bool bCompilation, + CAlbum::ReleaseType releaseType, + bool bScrapedMBID) +{ + if (idAlbum < 0) + return -1; + + // Art URLs limited on MySQL databases to 65535 characters (TEXT field) + // Truncate value cleaning up xml when URLs exceeds this + std::string strImageURLs = strImage; + if (StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, + "mysql")) + TrimImageURLs(strImageURLs, 65535); + + std::string strSQL; + strSQL = PrepareSQL("UPDATE album SET " + " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', " + " strMoods = '%s', strStyles = '%s', strThemes = '%s', " + " strReview = '%s', strImage = '%s', strLabel = '%s', " + " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i," + " strReleaseDate= '%s', strOrigReleaseDate= '%s', " + " bBoxedSet = %i, bCompilation = %i," + " strReleaseType = '%s', strReleaseStatus = '%s', " + " lastScraped = '%s', bScrapedMBID = %i", + strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), // + strMoods.c_str(), strStyles.c_str(), strThemes.c_str(), // + strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(), // + strType.c_str(), static_cast<double>(fRating), iUserrating, iVotes, // + strReleaseDate.c_str(), strOrigReleaseDate.c_str(), // + bBoxedSet, bCompilation, // + CAlbum::ReleaseTypeToString(releaseType).c_str(), strReleaseStatus.c_str(), // + CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID); + if (strMusicBrainzAlbumID.empty()) + strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL"); + else + strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str()); + if (strReleaseGroupMBID.empty()) + strSQL += PrepareSQL(", strReleaseGroupMBID = NULL"); + else + strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str()); + if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0) + strSQL += PrepareSQL(", strArtistSort = NULL"); + else + strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str()); + + strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum); + + bool status = ExecuteQuery(strSQL); + if (status) + AnnounceUpdate(MediaTypeAlbum, idAlbum); + return idAlbum; +} + +bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (idAlbum == -1) + return false; // not in the database + + //Get album, song and album song info data using separate queries/datasets because we can have + //multiple roles per artist for songs and that makes a single combined join impractical + //Get album data + std::string sql; + sql = PrepareSQL("SELECT albumview.*,albumartistview.* " + " FROM albumview " + " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum " + " WHERE albumview.idAlbum = %ld " + " ORDER BY albumartistview.iOrder", + idAlbum); + + CLog::Log(LOGDEBUG, "{}", sql); + if (!m_pDS->query(sql)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + int albumArtistOffset = album_enumCount; + + album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0, + true); // true to grab and parse the imageURL + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview) + // so there is only one row in the result set for each artist credit. + album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset)); + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + + //Get song data + if (getSongs) + { + sql = PrepareSQL("SELECT songview.*, songartistview.*" + " FROM songview " + " JOIN songartistview ON songview.idSong = songartistview.idSong " + " WHERE songview.idAlbum = %ld " + " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder", + idAlbum); + + CLog::Log(LOGDEBUG, "{}", sql); + if (!m_pDS->query(sql)) + return false; + if (m_pDS->num_rows() == 0) //Album with no songs + { + m_pDS->close(); + return false; + } + + int songArtistOffset = song_enumCount; + std::set<int> songs; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join + if (songs.find(idSong) == songs.end()) + { + album.songs.emplace_back(GetSongFromDataset(record)); + songs.insert(idSong); + } + + int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt(); + //By query order song is the last one appended to the album song vector. + if (idSongArtistRole == ROLE_ARTIST) + album.songs.back().artistCredits.emplace_back( + GetArtistCreditFromDataset(record, songArtistOffset)); + else + album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset)); + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum); + } + + return false; +} + +bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum) +{ + std::string strSQL = + PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum) +{ + std::string strSQL = + PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum); + return GetSingleValue(strSQL).empty(); +} + +int CMusicDatabase::AddGenre(std::string& strGenre) +{ + std::string strSQL; + try + { + StringUtils::Trim(strGenre); + + if (strGenre.empty()) + strGenre = g_localizeStrings.Get(13205); // Unknown + + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + auto it = m_genreCache.find(strGenre); + if (it != m_genreCache.end()) + return it->second; + + + strSQL = PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'", + strGenre.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL = PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )", + strGenre.c_str()); + m_pDS->exec(strSQL); + + int idGenre = (int)m_pDS->lastinsertid(); + m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre)); + return idGenre; + } + else + { + int idGenre = m_pDS->fv("idGenre").get_asInt(); + strGenre = m_pDS->fv("strGenre").get_asString(); + m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre)); + m_pDS->close(); + return idGenre; + } + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addgenre ({})", strSQL); + } + + return -1; +} + +bool CMusicDatabase::UpdateArtist(const CArtist& artist) +{ + SetLibraryLastUpdated(); + + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + UpdateArtist(artist.idArtist, // + artist.strArtist, // + artist.strSortName, // + artist.strMusicBrainzArtistID, // + artist.bScrapedMBID, // + artist.strType, // + artist.strGender, // + artist.strDisambiguation, // + artist.strBorn, // + artist.strFormed, // + StringUtils::Join(artist.genre, itemSeparator), // + StringUtils::Join(artist.moods, itemSeparator), // + StringUtils::Join(artist.styles, itemSeparator), // + StringUtils::Join(artist.instruments, itemSeparator), // + artist.strBiography, // + artist.strDied, // + artist.strDisbanded, // + StringUtils::Join(artist.yearsActive, itemSeparator).c_str(), // + artist.thumbURL.GetData()); + + DeleteArtistDiscography(artist.idArtist); + for (const auto& disc : artist.discography) + { + AddArtistDiscography(artist.idArtist, disc); + } + + // Set current artwork (held in art table) + if (!artist.art.empty()) + SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art); + + return true; +} + +int CMusicDatabase::AddArtist(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + const std::string& strSortName, + bool bScrapedMBID /* = false*/) +{ + std::string strSQL; + int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID); + if (idArtist < 0 || strSortName.empty()) + return idArtist; + + /* Artist sort name always taken as the first value provided that is different from name, so only + update when current sort name is blank. If a new sortname the same as name is provided then + clear any sortname currently held. + */ + + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist); + m_pDS->query(strSQL); + if (m_pDS->num_rows() != 1) + { + m_pDS->close(); + return -1; + } + std::string strArtistName, strArtistSort; + strArtistName = m_pDS->fv("strArtist").get_asString(); + strArtistSort = m_pDS->fv("strSortName").get_asString(); + m_pDS->close(); + + if (!strArtistSort.empty()) + { + if (strSortName.compare(strArtistName) == 0) + m_pDS->exec( + PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist)); + } + else if (strSortName.compare(strArtistName) != 0) + m_pDS->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i", + strSortName.c_str(), idArtist)); + + return idArtist; + } + + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname ({})", strSQL); + } + + return -1; +} + +int CMusicDatabase::AddArtist(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + bool bScrapedMBID /* = false*/) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + // 1) MusicBrainz + if (!strMusicBrainzArtistID.empty()) + { + // 1.a) Match on a MusicBrainz ID + strSQL = + PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'", + strMusicBrainzArtistID.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + { + int idArtist = m_pDS->fv("idArtist").get_asInt(); + bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0; + m_pDS->close(); + if (update) + { + strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' " + "WHERE idArtist = %i", + strArtist.c_str(), idArtist); + m_pDS->exec(strSQL); + m_pDS->close(); + } + return idArtist; + } + m_pDS->close(); + + + // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID + // and update that if it exists. + strSQL = PrepareSQL("SELECT idArtist FROM artist " + "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL", + strArtist.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + { + int idArtist = m_pDS->fv("idArtist").get_asInt(); + m_pDS->close(); + // 1.b.a) We found an artist by name but with no MusicBrainz ID set, update it and assume it is our artist, flag when mbid scraped + strSQL = + PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', " + "bScrapedMBID = %i WHERE idArtist = %i", + strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID, idArtist); + m_pDS->exec(strSQL); + return idArtist; + } + + // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name. + // With MusicBrainz IDs this could return multiple artists and is non-determinstic + // Always pick the first artist ID returned by the DB to return. + } + else + { + strSQL = + PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str()); + + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + { + int idArtist = m_pDS->fv("idArtist").get_asInt(); + m_pDS->close(); + return idArtist; + } + m_pDS->close(); + } + + // 3) No artist exists at all - add it, flagging when has scraped mbid + if (strMusicBrainzArtistID.empty()) + strSQL = PrepareSQL("INSERT INTO artist " + "(idArtist, strArtist, strMusicBrainzArtistID) " + "VALUES( NULL, '%s', NULL)", + strArtist.c_str()); + else + strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, " + "bScrapedMBID) " + "VALUES( NULL, '%s', '%s', %i )", + strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID); + + m_pDS->exec(strSQL); + int idArtist = (int)m_pDS->lastinsertid(); + return idArtist; + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addartist ({})", strSQL); + } + + return -1; +} + +int CMusicDatabase::UpdateArtist(int idArtist, + const std::string& strArtist, + const std::string& strSortName, + const std::string& strMusicBrainzArtistID, + const bool bScrapedMBID, + const std::string& strType, + const std::string& strGender, + const std::string& strDisambiguation, + const std::string& strBorn, + const std::string& strFormed, + const std::string& strGenres, + const std::string& strMoods, + const std::string& strStyles, + const std::string& strInstruments, + const std::string& strBiography, + const std::string& strDied, + const std::string& strDisbanded, + const std::string& strYearsActive, + const std::string& strImage) +{ + if (idArtist < 0) + return -1; + + // Check another artist with this mbid not already exist (an alias for example) + bool useMBIDNull = strMusicBrainzArtistID.empty(); + bool isScrapedMBID = bScrapedMBID; + std::string artistname; + int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname); + if (idArtistMbid > 0 && idArtistMbid != idArtist) + { + CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})", + __FUNCTION__, strMusicBrainzArtistID, artistname, idArtistMbid, strArtist, idArtist); + useMBIDNull = true; + isScrapedMBID = false; + } + + // Art URLs limited on MySQL databases to 65535 characters (TEXT field) + // Truncate value cleaning up xml when URLs exceeds this + std::string strImageURLs = strImage; + if (StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, + "mysql")) + TrimImageURLs(strImageURLs, 65535); + + std::string strSQL; + strSQL = PrepareSQL("UPDATE artist SET " + " strArtist = '%s', " + " strType = '%s', strGender = '%s', strDisambiguation = '%s', " + " strBorn = '%s', strFormed = '%s', strGenres = '%s', " + " strMoods = '%s', strStyles = '%s', strInstruments = '%s', " + " strBiography = '%s', strDied = '%s', strDisbanded = '%s', " + " strYearsActive = '%s', strImage = '%s', " + " lastScraped = '%s', bScrapedMBID = %i", + strArtist.c_str(), + /* strSortName.c_str(),*/ + /* strMusicBrainzArtistID.c_str(), */ + strType.c_str(), strGender.c_str(), strDisambiguation.c_str(), // + strBorn.c_str(), strFormed.c_str(), strGenres.c_str(), // + strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(), // + strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(), // + strYearsActive.c_str(), strImageURLs.c_str(), // + CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID); + if (useMBIDNull) + strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL"); + else + strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str()); + if (strSortName.empty()) + strSQL += PrepareSQL(", strSortName = NULL"); + else + strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str()); + + strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist); + + bool status = ExecuteQuery(strSQL); + if (status) + AnnounceUpdate(MediaTypeArtist, idArtist); + return idArtist; +} + +bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist, + const std::string& strMusicBrainzArtistID) +{ + if (strMusicBrainzArtistID.empty() || idArtist < 0) + return false; + + // Check another artist with this mbid not already exist (an alias for example) + std::string artistname; + int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname); + if (idArtistMbid > 0 && idArtistMbid != idArtist) + { + CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__, + strMusicBrainzArtistID, artistname, idArtistMbid); + return false; + } + + // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID + std::string strSQL; + strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 " + "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL", + strMusicBrainzArtistID.c_str(), idArtist); + + bool status = ExecuteQuery(strSQL); + if (status) + { + AnnounceUpdate(MediaTypeArtist, idArtist); + return true; + } + return false; +} + +bool CMusicDatabase::GetArtist(int idArtist, CArtist& artist, bool fetchAll /* = false */) +{ + try + { + auto start = std::chrono::steady_clock::now(); + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (idArtist == -1) + return false; // not in the database + + std::string strSQL; + if (fetchAll) + strSQL = PrepareSQL("SELECT * FROM artistview " + "LEFT JOIN discography ON artistview.idArtist = discography.idArtist " + "WHERE artistview.idArtist = %i", + idArtist); + else + strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + int discographyOffset = artist_enumCount; + + artist.discography.clear(); + artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs + if (fetchAll) + { + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + CDiscoAlbum discoAlbum; + discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString(); + discoAlbum.strYear = record->at(discographyOffset + 2).get_asString(); + discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString(); + artist.discography.emplace_back(discoAlbum); + m_pDS->next(); + } + } + m_pDS->close(); // cleanup recordset data + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL, + duration.count()); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + + return false; +} + +bool CMusicDatabase::GetArtistExists(int idArtist) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = + PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + + return false; +} + +int CMusicDatabase::GetLastArtist() +{ + std::string strSQL = "SELECT MAX(idArtist) FROM artist"; + std::string lastArtist = GetSingleValue(strSQL); + if (lastArtist.empty()) + return -1; + + return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10)); +} + +int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID, + std::string& artistname) +{ + if (strMusicBrainzArtistID.empty()) + return -1; + + std::string strSQL; + try + { + if (nullptr == m_pDB || nullptr == m_pDS2) + return -1; + // Match on MusicBrainz ID, definitively unique + strSQL = + PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'", + strMusicBrainzArtistID.c_str()); + if (!m_pDS2->query(strSQL)) + return -1; + int idArtist = -1; + if (m_pDS2->num_rows() > 0) + { + idArtist = m_pDS2->fv("idArtist").get_asInt(); + artistname = m_pDS2->fv("strArtist").get_asString(); + } + m_pDS2->close(); + return idArtist; + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__, strSQL); + } + return -1; +} + +bool CMusicDatabase::HasArtistBeenScraped(int idArtist) +{ + std::string strSQL = PrepareSQL( + "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist); + return GetSingleValue(strSQL).empty(); +} + +bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist) +{ + std::string strSQL = + PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist); + return ExecuteQuery(strSQL); +} + +int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum) +{ + std::string strSQL = PrepareSQL("INSERT INTO discography " + "(idArtist, strAlbum, strYear, strReleaseGroupMBID) " + "VALUES(%i, '%s', '%s', '%s')", + idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(), + discoAlbum.strReleaseGroupMBID.c_str()); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::DeleteArtistDiscography(int idArtist) +{ + std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + /* Combine entries from discography and album tables + Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using + correlated subqueries to a temp table. An updatable join to temp table would work in MySQL + but SQLite not support updatable joins. + */ + m_pDS->exec("CREATE TABLE tempDisco " + "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)"); + m_pDS->exec("CREATE TABLE tempAlbum " + "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)"); + + std::string strSQL; + strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) " + "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, " + "strReleaseGroupMBID, NULL " + "FROM discography WHERE idArtist = %i", + idArtist); + m_pDS->exec(strSQL); + + strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) " + "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, " + "strReleaseGroupMBID, album.idAlbum " + "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum " + "WHERE idArtist = %i", + idArtist); + m_pDS->exec(strSQL); + + // Match albums on release group mbid, if multi-releases then first used + // Only use albums credited to this artist + strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum " + "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)"; + m_pDS->exec(strSQL); + //Delete matched albums + strSQL = "DELETE FROM tempAlbum " + "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)"; + m_pDS->exec(strSQL); + + // Match remaining to albums by artist on title and year + strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum " + "WHERE tempAlbum.strAlbum = tempDisco.strAlbum " + "AND tempAlbum.strYear = tempDisco.strYear) " + "WHERE tempDisco.idAlbum is NULL"; + m_pDS->exec(strSQL); + //Delete matched albums + strSQL = "DELETE FROM tempAlbum " + "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)"; + m_pDS->exec(strSQL); + + // Match remaining to albums by artist on title only + strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum " + "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) " + "WHERE tempDisco.idAlbum is NULL"; + m_pDS->exec(strSQL); + // Use year from album table, when matched by title only as it could be different + strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum " + "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) " + "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)"; + m_pDS->exec(strSQL); + //Delete matched albums + strSQL = "DELETE FROM tempAlbum " + "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)"; + m_pDS->exec(strSQL); + + // Combine distinctly with any remaining unmatched albums by artist + strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco " + "UNION " + "SELECT strAlbum, strYear, idAlbum FROM tempAlbum " + "ORDER BY strYear, strAlbum, idAlbum"; + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + while (!m_pDS->eof()) + { + int idAlbum = m_pDS->fv("idAlbum").get_asInt(); + if (idAlbum == 0) + idAlbum = -1; + std::string strAlbum = m_pDS->fv("strAlbum").get_asString(); + if (!strAlbum.empty()) + { + CFileItemPtr pItem(new CFileItem(strAlbum)); + pItem->SetLabel2(m_pDS->fv("strYear").get_asString()); + pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum); + items.Add(pItem); + } + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + m_pDS->exec("DROP TABLE tempDisco"); + m_pDS->exec("DROP TABLE tempAlbum"); + + return true; + } + catch (...) + { + m_pDS->exec("DROP TABLE tempDisco"); + m_pDS->exec("DROP TABLE tempAlbum"); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +int CMusicDatabase::AddRole(const std::string& strRole) +{ + int idRole = -1; + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + idRole = m_pDS->fv("idRole").get_asInt(); + m_pDS->close(); + + if (idRole < 0) + { + strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str()); + m_pDS->exec(strSQL); + idRole = static_cast<int>(m_pDS->lastinsertid()); + m_pDS->close(); + } + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to AddRole ({})", strSQL); + } + return idRole; +} + +bool CMusicDatabase::AddSongArtist( + int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder) +{ + int idRole = AddRole(strRole); + return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder); +} + +bool CMusicDatabase::AddSongArtist( + int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder) +{ + std::string strSQL; + strSQL = PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) " + "VALUES(%i, %i, %i,'%s', %i)", + idArtist, idSong, idRole, strArtist.c_str(), iOrder); + return ExecuteQuery(strSQL); +} + +int CMusicDatabase::AddSongContributor(int idSong, + const std::string& strRole, + const std::string& strArtist, + const std::string& strSort) +{ + if (strArtist.empty()) + return -1; + + std::string strSQL; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + int idArtist = -1; + // Add artist. As we only have name (no MBID) first try to identify artist from song + // as they may have already been added with a different role (including MBID). + strSQL = + PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ", + idSong, strArtist.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + idArtist = m_pDS->fv("idArtist").get_asInt(); + m_pDS->close(); + + if (idArtist < 0) + idArtist = AddArtist(strArtist, "", strSort); + + // Add to song_artist table + AddSongArtist(idArtist, idSong, strRole, strArtist, 0); + + return idArtist; + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor ({})", strSQL); + } + + return -1; +} + +void CMusicDatabase::AddSongContributors(int idSong, + const VECMUSICROLES& contributors, + const std::string& strSort) +{ + std::vector<std::string> composerSort; + size_t countComposer = 0; + if (!strSort.empty()) + { + composerSort = StringUtils::Split( + strSort, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + } + + for (const auto& credit : contributors) + { + std::string strSortName; + //Identify composer sort name if we have it + if (countComposer < composerSort.size()) + { + if (credit.GetRoleDesc().compare("Composer") == 0) + { + strSortName = composerSort[countComposer]; + countComposer++; + } + } + AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName); + } +} + +int CMusicDatabase::GetRoleByName(const std::string& strRole) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return -1; + } + return m_pDS->fv("idRole").get_asInt(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item) +{ + try + { + std::string strSQL = + PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole " + "FROM song_artist JOIN role ON song_artist.idRole = role.idRole " + "WHERE idArtist = %i ORDER BY song_artist.idRole ASC", + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return true; + } + + CVariant artistRoles(CVariant::VariantTypeArray); + + while (!m_pDS->eof()) + { + CVariant roleObj; + roleObj["role"] = m_pDS->fv("strRole").get_asString(); + roleObj["roleid"] = m_pDS->fv("idrole").get_asInt(); + artistRoles.push_back(roleObj); + m_pDS->next(); + } + m_pDS->close(); + + item->SetProperty("roles", artistRoles); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +} + +bool CMusicDatabase::DeleteSongArtistsBySong(int idSong) +{ + return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong)); +} + +bool CMusicDatabase::AddAlbumArtist(int idArtist, + int idAlbum, + const std::string& strArtist, + int iOrder) +{ + std::string strSQL; + strSQL = PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) " + "VALUES(%i,%i,'%s',%i)", + idArtist, idAlbum, strArtist.c_str(), iOrder); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum) +{ + return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum)); +} + +bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres) +{ + if (idSong == -1) + return true; + + std::string strSQL; + try + { + // Clear current entries for song + strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong); + if (!ExecuteQuery(strSQL)) + return false; + unsigned int index = 0; + std::vector<std::string> modgenres = genres; + for (auto& strGenre : modgenres) + { + int idGenre = AddGenre(strGenre); // Genre string trimmed and matched case-insensitively + strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)", + idGenre, idSong, index++); + if (!ExecuteQuery(strSQL)) + return false; + } + // Update concatenated genre string from the standardised genre values + std::string strGenres = StringUtils::Join( + modgenres, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", // + strGenres.c_str(), idSong); + if (!ExecuteQuery(strSQL)) + return false; + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) {} failed", __FUNCTION__, idSong, strSQL); + } + return false; +} + +bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int>& albums) +{ + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + while (!m_pDS->eof()) + { + albums.push_back(m_pDS->fv("idAlbum").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +} + +bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item) +{ + try + { + std::string strSQL; + + strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + // Get album artist credits + VECARTISTCREDITS artistCredits; + while (!m_pDS->eof()) + { + artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0)); + m_pDS->next(); + } + m_pDS->close(); + + // Populate item with song albumartist credits + std::vector<std::string> musicBrainzID; + std::vector<std::string> albumartists; + CVariant artistidObj(CVariant::VariantTypeArray); + for (const auto& artistCredit : artistCredits) + { + artistidObj.push_back(artistCredit.GetArtistId()); + albumartists.emplace_back(artistCredit.GetArtist()); + if (!artistCredit.GetMusicBrainzArtistID().empty()) + musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID()); + } + item->GetMusicInfoTag()->SetAlbumArtist(albumartists); + item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID); + // Add song albumartistIds as separate property as not part of CMusicInfoTag + item->SetProperty("albumartistid", artistidObj); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum); + } + return false; +} + +bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs) +{ + try + { + std::string strSQL; + // Get distinct song and album artist IDs for this album, no other roles + // Allow for artists that are only album artists and not song artists + strSQL = PrepareSQL( + "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n" + "UNION \n" + "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong " + "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ", + idAlbum, idAlbum); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + while (!m_pDS->eof()) + { + // Get ID as string so can easily join to make "IN" clause + artistIDs.push_back(m_pDS->fv("idArtist").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum); + } + return false; +}; + +bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int>& songs) +{ + try + { + std::string strSQL; + //Restrict to Artists only, no other roles + strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", // + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + while (!m_pDS->eof()) + { + songs.push_back(m_pDS->fv("idSong").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +}; + +bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int>& artists) +{ + try + { + std::string strSQL; + //Restrict to Artists only, no other roles + strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", // + idSong); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + while (!m_pDS->eof()) + { + artists.push_back(m_pDS->fv("idArtist").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong); + } + return false; +} + +bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item) +{ + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre " + "FROM album_artist " + "JOIN song ON album_artist.idAlbum = song.idAlbum " + "JOIN song_genre ON song.idSong = song_genre.idSong " + "JOIN genre ON song_genre.idGenre = genre.idGenre " + "WHERE album_artist.idArtist = %i " + "ORDER BY song_genre.idGenre", + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + // Artist does have any song genres via albums may not be an album artist. + // Check via songs artist to fetch song genres from compilations or where they are guest artist + m_pDS->close(); + strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre " + "FROM song_artist " + "JOIN song_genre ON song_artist.idSong = song_genre.idSong " + "JOIN genre ON song_genre.idGenre = genre.idGenre " + "WHERE song_artist.idArtist = %i " + "ORDER BY song_genre.idGenre", + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + //No song genres, but query successful + m_pDS->close(); + return true; + } + } + + CVariant artistSongGenres(CVariant::VariantTypeArray); + + while (!m_pDS->eof()) + { + CVariant genreObj; + genreObj["title"] = m_pDS->fv("strGenre").get_asString(); + genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt(); + artistSongGenres.push_back(genreObj); + m_pDS->next(); + } + m_pDS->close(); + + item->SetProperty("songgenres", artistSongGenres); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +} + +bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item) +{ + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM " + "song JOIN song_genre ON song.idSong = song_genre.idSong " + "JOIN genre ON song_genre.idGenre = genre.idGenre " + "WHERE song.idAlbum = %i " + "ORDER BY song_genre.idSong, song_genre.iOrder", + idAlbum); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + //No song genres, but query successful + m_pDS->close(); + return true; + } + + CVariant albumSongGenres(CVariant::VariantTypeArray); + + while (!m_pDS->eof()) + { + CVariant genreObj; + genreObj["title"] = m_pDS->fv("strGenre").get_asString(); + genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt(); + albumSongGenres.push_back(genreObj); + m_pDS->next(); + } + m_pDS->close(); + + item->SetProperty("songgenres", albumSongGenres); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum); + } + return false; +} + +bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres) +{ + try + { + std::string strSQL = PrepareSQL("SELECT idGenre FROM song_genre " + "WHERE idSong = %i ORDER BY iOrder ASC", + idSong); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return true; + } + + while (!m_pDS->eof()) + { + genres.push_back(m_pDS->fv("idGenre").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong); + } + return false; +} + +bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item) +{ + try + { + int countalbum = + GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist)); + CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean); + IsAlbumArtistObj = (countalbum > 0); + item->SetProperty("isalbumartist", IsAlbumArtistObj); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +} + + +int CMusicDatabase::AddPath(const std::string& strPath1) +{ + std::string strSQL; + try + { + std::string strPath(strPath1); + if (!URIUtils::HasSlashAtEnd(strPath)) + URIUtils::AddSlashAtEnd(strPath); + + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + auto it = m_pathCache.find(strPath); + if (it != m_pathCache.end()) + return it->second; + + strSQL = PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL = PrepareSQL("INSERT INTO path (idPath, strPath) " + "VALUES(NULL, '%s')", + strPath.c_str()); + m_pDS->exec(strSQL); + + int idPath = (int)m_pDS->lastinsertid(); + m_pathCache.insert(std::pair<std::string, int>(strPath, idPath)); + return idPath; + } + else + { + int idPath = m_pDS->fv("idPath").get_asInt(); + m_pathCache.insert(std::pair<std::string, int>(strPath, idPath)); + m_pDS->close(); + return idPath; + } + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addpath ({})", strSQL); + } + + return -1; +} + +CSong CMusicDatabase::GetSongFromDataset() +{ + return GetSongFromDataset(m_pDS->get_sql_record()); +} + +CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record, + int offset /* = 0 */) +{ + CSong song; + song.idSong = record->at(offset + song_idSong).get_asInt(); + // Note this function does not populate artist credits, this must be done separately. + // However artist names are held as a descriptive string + song.strArtistDesc = record->at(offset + song_strArtists).get_asString(); + song.strArtistSort = record->at(offset + song_strArtistSort).get_asString(); + // Get the full genre string + song.genre = StringUtils::Split( + record->at(offset + song_strGenres).get_asString(), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + // and the rest... + song.strAlbum = record->at(offset + song_strAlbum).get_asString(); + song.idAlbum = record->at(offset + song_idAlbum).get_asInt(); + song.iTrack = record->at(offset + song_iTrack).get_asInt(); + song.iDuration = record->at(offset + song_iDuration).get_asInt(); + song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString(); + song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString(); + song.strTitle = record->at(offset + song_strTitle).get_asString(); + song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt(); + song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString()); + song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString()); + song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString()); + song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString()); + song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt(); + song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt(); + song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString(); + song.rating = record->at(offset + song_rating).get_asFloat(); + song.userrating = record->at(offset + song_userrating).get_asInt(); + song.votes = record->at(offset + song_votes).get_asInt(); + song.strComment = record->at(offset + song_comment).get_asString(); + song.strMood = record->at(offset + song_mood).get_asString(); + song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1; + song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString(); + // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata) + song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString()); + // Get filename with full path + song.strFileName = + URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(), + record->at(offset + song_strFileName).get_asString()); + song.iBPM = record->at(offset + song_iBPM).get_asInt(); + song.iBitRate = record->at(offset + song_iBitRate).get_asInt(); + song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt(); + song.iChannels = record->at(offset + song_iChannels).get_asInt(); + return song; +} + +void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl) +{ + GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl); +} + +void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record, + CFileItem* item, + const CMusicDbUrl& baseUrl) +{ + // get the artist string from songview (not the song_artist and artist tables) + item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString()); + // get the artist sort name string from songview (not the song_artist and artist tables) + item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString()); + // and the full genre string + item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString()); + // and the rest... + item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString()); + item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt()); + item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt()); + item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt()); + item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong); + item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString()); + item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString()); + item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString()); + item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString()); + item->SetLabel(record->at(song_strTitle).get_asString()); + item->SetStartOffset(record->at(song_iStartOffset).get_asInt64()); + item->SetProperty("item_start", item->GetStartOffset()); + item->SetEndOffset(record->at(song_iEndOffset).get_asInt64()); + item->GetMusicInfoTag()->SetMusicBrainzTrackID( + record->at(song_strMusicBrainzTrackID).get_asString()); + item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat()); + item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt()); + item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt()); + item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString()); + item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString()); + item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt()); + item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString()); + item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString()); + item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString()); + item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString()); + std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(), + record->at(song_strFileName).get_asString()); + item->GetMusicInfoTag()->SetURL(strRealPath); + item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1); + item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1); + // get the album artist string from songview (not the album_artist and artist tables) + item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString()); + item->GetMusicInfoTag()->SetAlbumReleaseType( + CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString())); + item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt()); + item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt()); + item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt()); + item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt()); + // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata) + ReplayGain replaygain; + replaygain.Set(record->at(song_strReplayGain).get_asString()); + item->GetMusicInfoTag()->SetReplayGain(replaygain); + item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt()); + + item->GetMusicInfoTag()->SetLoaded(true); + // Get filename with full path + if (!baseUrl.IsValid()) + item->SetPath(strRealPath); + else + { + CMusicDbUrl itemUrl = baseUrl; + std::string strFileName = record->at(song_strFileName).get_asString(); + std::string strExt = URIUtils::GetExtension(strFileName); + std::string path = StringUtils::Format("{}{}", record->at(song_idSong).get_asInt(), strExt); + itemUrl.AppendPath(path); + item->SetPath(itemUrl.ToString()); + item->SetDynPath(strRealPath); + } +} + +void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item) +{ + // Populate fileitem with artists from vector of artist credits + std::vector<std::string> musicBrainzID; + std::vector<std::string> songartists; + CVariant artistidObj(CVariant::VariantTypeArray); + + // When "missing tag" artist, it is the only artist when present. + if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID) + { + artistidObj.push_back((int)BLANKARTIST_ID); + songartists.push_back(StringUtils::Empty); + } + else + { + for (const auto& artistCredit : artistCredits) + { + artistidObj.push_back(artistCredit.GetArtistId()); + songartists.push_back(artistCredit.GetArtist()); + if (!artistCredit.GetMusicBrainzArtistID().empty()) + musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID()); + } + } + // Also sets ArtistDesc if empty from song.strArtist field + item->GetMusicInfoTag()->SetArtist(songartists); + item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID); + // Add album artistIds as separate property as not part of CMusicInfoTag + item->SetProperty("artistid", artistidObj); +} + +CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS, + int offset /* = 0 */, + bool imageURL /* = false*/) +{ + return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL); +} + +CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record, + int offset /* = 0 */, + bool imageURL /* = false*/) +{ + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + CAlbum album; + album.idAlbum = record->at(offset + album_idAlbum).get_asInt(); + album.strAlbum = record->at(offset + album_strAlbum).get_asString(); + if (album.strAlbum.empty()) + album.strAlbum = g_localizeStrings.Get(1050); + album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString(); + album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString(); + album.strArtistDesc = record->at(offset + album_strArtists).get_asString(); + album.strArtistSort = record->at(offset + album_strArtistSort).get_asString(); + album.genre = + StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator); + album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString(); + album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString(); + album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1; + if (imageURL) + album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString()); + album.fRating = record->at(offset + album_fRating).get_asFloat(); + album.iUserrating = record->at(offset + album_iUserrating).get_asInt(); + album.iVotes = record->at(offset + album_iVotes).get_asInt(); + album.strReview = record->at(offset + album_strReview).get_asString(); + album.styles = + StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator); + album.moods = + StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator); + album.themes = + StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator); + album.strLabel = record->at(offset + album_strLabel).get_asString(); + album.strType = record->at(offset + album_strType).get_asString(); + album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString(); + album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1; + album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1; + album.strLastScraped = record->at(offset + album_lastScraped).get_asString(); + album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt(); + album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString()); + album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt(); + album.SetDateAdded(record->at(offset + album_dateAdded).get_asString()); + album.SetDateNew(record->at(offset + album_dateNew).get_asString()); + album.SetDateUpdated(record->at(offset + album_dateModified).get_asString()); + album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString()); + album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt(); + return album; +} + +CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record, + int offset /* = 0 */) +{ + CArtistCredit artistCredit; + artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt(); + if (artistCredit.idArtist == BLANKARTIST_ID) + artistCredit.m_strArtist = StringUtils::Empty; + else + { + artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString(); + artistCredit.m_strMusicBrainzArtistID = + record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString(); + } + return artistCredit; +} + +CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record, + int offset /* = 0 */) +{ + CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(), + record->at(offset + artistCredit_strRole).get_asString(), + record->at(offset + artistCredit_strArtist).get_asString(), + record->at(offset + artistCredit_idArtist).get_asInt()); + return ArtistRole; +} + +CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS, + int offset /* = 0 */, + bool needThumb /* = true */) +{ + return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb); +} + +CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record, + int offset /* = 0 */, + bool needThumb /* = true */) +{ + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + CArtist artist; + artist.idArtist = record->at(offset + artist_idArtist).get_asInt(); + if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist) + artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language + else + artist.strArtist = record->at(offset + artist_strArtist).get_asString(); + artist.strSortName = record->at(offset + artist_strSortName).get_asString(); + artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString(); + artist.strType = record->at(offset + artist_strType).get_asString(); + artist.strGender = record->at(offset + artist_strGender).get_asString(); + artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString(); + artist.genre = + StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator); + artist.strBiography = record->at(offset + artist_strBiography).get_asString(); + artist.styles = + StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator); + artist.moods = + StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator); + artist.strBorn = record->at(offset + artist_strBorn).get_asString(); + artist.strFormed = record->at(offset + artist_strFormed).get_asString(); + artist.strDied = record->at(offset + artist_strDied).get_asString(); + artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString(); + artist.yearsActive = + StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator); + artist.instruments = + StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator); + artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1; + artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString(); + artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString()); + artist.SetDateNew(record->at(offset + artist_dateNew).get_asString()); + artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString()); + + if (needThumb) + { + artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString()); + } + + return artist; +} + +bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath, + CSong& song, + int64_t startOffset) +{ + song.Clear(); + CURL url(strFileNameAndPath); + + if (url.IsProtocol("musicdb")) + { + std::string strFile = URIUtils::GetFileName(strFileNameAndPath); + URIUtils::RemoveExtension(strFile); + return GetSong(atoi(strFile.c_str()), song); + } + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strPath, strFileName; + SplitPath(strFileNameAndPath, strPath, strFileName); + URIUtils::AddSlashAtEnd(strPath); + + std::string strSQL = PrepareSQL("SELECT idSong FROM songview " + "WHERE strFileName='%s' AND strPath='%s'", + strFileName.c_str(), strPath.c_str()); + if (startOffset) + strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset); + + int idSong = GetSingleValueInt(strSQL); + if (idSong > 0) + return GetSong(idSong, song); + + return false; +} + +int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song " + "JOIN path ON song.idPath = path.idPath " + "WHERE path.strPath='%s'", + strPath.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + + int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned + if (iRowsFound == 1) + idAlbum = m_pDS->fv(0).get_asInt(); + + m_pDS->close(); + + return idAlbum; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath); + } + + return -1; +} + +int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist, + const std::string& strAlbum, + const std::string& strTitle) +{ + try + { + std::string strSQL = + PrepareSQL("SELECT idSong FROM songview " + "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'", + strArtist.c_str(), strAlbum.c_str(), strTitle.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return -1; + } + int lResult = m_pDS->fv(0).get_asInt(); + m_pDS->close(); // cleanup recordset data + return lResult; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({},{},{}) failed", __FUNCTION__, strArtist, strAlbum, strTitle); + } + + return -1; +} + +bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList& artists) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strVariousArtists = g_localizeStrings.Get(340).c_str(); + std::string strSQL; + if (search.size() >= MIN_FULL_SEARCH_LENGTH) + strSQL = PrepareSQL("SELECT * FROM artist " + "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') " + "AND strArtist <> '%s' ", + search.c_str(), search.c_str(), strVariousArtists.c_str()); + else + strSQL = PrepareSQL("SELECT * FROM artist " + "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ", + search.c_str(), strVariousArtists.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + + const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist + while (!m_pDS->eof()) + { + std::string path = StringUtils::Format("musicdb://artists/{}/", m_pDS->fv(0).get_asInt()); + CFileItemPtr pItem(new CFileItem(path, true)); + std::string label = StringUtils::Format("[{}] {}", artistLabel, m_pDS->fv(1).get_asString()); + pItem->SetLabel(label); + // sort label is stored in the title tag + label = StringUtils::Format("A {}", m_pDS->fv(1).get_asString()); + pItem->GetMusicInfoTag()->SetTitle(label); + pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist); + artists.Add(pItem); + m_pDS->next(); + } + + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CMusicDbUrl baseUrl; + if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir)) + return false; + + std::string strSQL = "SELECT * FROM songview " + "WHERE iTimesPlayed>0 " + "ORDER BY iTimesPlayed DESC " + "LIMIT 100"; + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + items.Reserve(iRowsFound); + while (!m_pDS->eof()) + { + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(item.get(), baseUrl); + items.Add(item); + m_pDS->next(); + } + + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums) +{ + try + { + albums.erase(albums.begin(), albums.end()); + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Get data from album and album_artist tables to fully populate albums + std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview " + "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum " + "WHERE albumartistview.idAlbum IN " + "(SELECT albumview.idAlbum FROM albumview " + "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 " + "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) " + "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder"; + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + int albumArtistOffset = album_enumCount; + int albumId = -1; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (albumId != record->at(album_idAlbum).get_asInt()) + { // New album + albumId = record->at(album_idAlbum).get_asInt(); + albums.push_back(GetAlbumFromDataset(record)); + } + // Get album artists + albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset)); + + m_pDS->next(); + } + + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CMusicDbUrl baseUrl; + if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir)) + return false; + + std::string strSQL = StringUtils::Format( + "SELECT songview.*, albumview.* FROM songview" + "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) " + "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song " + "WHERE song.iTimesPlayed > 0 " + "GROUP BY idAlbum " + "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit " + "ON (songview.idAlbum = _albumlimit.idAlbum) " + "ORDER BY _albumlimit.iTimesPlayedSum DESC"); + CLog::Log(LOGDEBUG, "GetTop100AlbumSongs() query: {}", strSQL); + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // get data from returned rows + items.Reserve(iRowsFound); + while (!m_pDS->eof()) + { + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(item.get(), baseUrl); + items.Add(item); + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums) +{ + try + { + albums.erase(albums.begin(), albums.end()); + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + auto start = std::chrono::steady_clock::now(); + + // Get data from album and album_artist tables to fully populate albums + std::string strSQL = + PrepareSQL("SELECT albumview.*, albumartistview.* " + "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL " + "AND albumview.strReleaseType = '%s' " + "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums " + "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum " + "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum " + "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ", + CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT); + + auto queryStart = std::chrono::steady_clock::now(); + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + + auto queryEnd = std::chrono::steady_clock::now(); + auto queryDuration = + std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart); + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + int albumArtistOffset = album_enumCount; + int albumId = -1; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (albumId != record->at(album_idAlbum).get_asInt()) + { // New album + albumId = record->at(album_idAlbum).get_asInt(); + albums.push_back(GetAlbumFromDataset(record)); + } + // Get album artists + albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset)); + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__, + duration.count(), queryDuration.count()); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, + CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CMusicDbUrl baseUrl; + if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir)) + return false; + + std::string strSQL = + PrepareSQL("SELECT songview.*, songartistview.* " + "FROM (SELECT idAlbum, lastPlayed FROM albumview " + "WHERE albumview.lastplayed IS NOT NULL " + "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums " + "JOIN songview ON songview.idAlbum = playedalbums.idAlbum " + "JOIN songartistview ON songview.idSong = songartistview.idSong " + "ORDER BY playedalbums.lastplayed DESC, " + "songartistview.idsong, songartistview.idRole, songartistview.iOrder", + CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_iMusicLibraryRecentlyAddedItems); + CLog::Log(LOGDEBUG, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL); + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // Needs a separate query to determine number of songs to set items size. + // Get songs from returned rows. Join means there is a row for every song artist + // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too + int songArtistOffset = song_enumCount; + int songId = -1; + VECARTISTCREDITS artistCredits; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt(); + if (songId != record->at(song_idSong).get_asInt()) + { //New song + if (songId > 0 && !artistCredits.empty()) + { + //Store artist credits for previous song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + songId = record->at(song_idSong).get_asInt(); + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(record, item.get(), baseUrl); + items.Add(item); + } + // Get song artist credits and contributors + if (idSongArtistRole == ROLE_ARTIST) + artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset)); + else + items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole( + GetArtistRoleFromDataset(record, songArtistOffset)); + + m_pDS->next(); + } + if (!artistCredits.empty()) + { + //Store artist credits for final song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit) +{ + try + { + albums.erase(albums.begin(), albums.end()); + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Get data from album and album_artist tables to fully populate albums + // Determine the recently added albums from dateAdded (usually derived from music file + // timestamps, nothing to do with when albums added to library) + std::string strSQL = + PrepareSQL("SELECT albumview.*, albumartistview.* " + "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' " + "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums " + "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum " + "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum " + "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ", + limit ? limit + : CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_iMusicLibraryRecentlyAddedItems); + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + int albumArtistOffset = album_enumCount; + int albumId = -1; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (albumId != record->at(album_idAlbum).get_asInt()) + { // New album + albumId = record->at(album_idAlbum).get_asInt(); + albums.push_back(GetAlbumFromDataset(record)); + } + // Get album artists + albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset)); + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir, + CFileItemList& items, + unsigned int limit) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CMusicDbUrl baseUrl; + if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir)) + return false; + + // Get data from song and song_artist tables to fully populate songs + // Determine the recently added albums from dateAdded (usually derived from music file + // timestamps, nothing to do with when albums added to library) + std::string strSQL; + strSQL = PrepareSQL("SELECT songview.*, songartistview.* " + "FROM (SELECT idAlbum, dateAdded FROM album " + "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums " + "JOIN songview ON songview.idAlbum = recentalbums.idAlbum " + "JOIN songartistview ON songview.idSong = songartistview.idSong " + "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, " + "songview.idSong, songartistview.idRole, songartistview.iOrder ", + limit ? limit + : CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_iMusicLibraryRecentlyAddedItems); + CLog::Log(LOGDEBUG, "GetRecentlyAddedAlbumSongs() query: {}", strSQL); + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // Needs a separate query to determine number of songs to set items size. + // Get songs from returned rows. Join means there is a row for every song artist + int songArtistOffset = song_enumCount; + int songId = -1; + VECARTISTCREDITS artistCredits; + while (!m_pDS->eof()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt(); + if (songId != record->at(song_idSong).get_asInt()) + { //New song + if (songId > 0 && !artistCredits.empty()) + { + //Store artist credits for previous song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + songId = record->at(song_idSong).get_asInt(); + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(record, item.get(), baseUrl); + items.Add(item); + } + // Get song artist credits and contributors + if (idSongArtistRole == ROLE_ARTIST) + artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset)); + else + items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole( + GetArtistRoleFromDataset(record, songArtistOffset)); + + m_pDS->next(); + } + if (!artistCredits.empty()) + { + //Store artist credits for final song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +void CMusicDatabase::IncrementPlayCount(const CFileItem& item) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + int idSong = GetSongIDFromPath(item.GetPath()); + std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime(); + std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' " + "WHERE idSong=%i", + strDateNow.c_str(), idSong); + m_pDS->exec(sql); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, item.GetPath()); + } +} + +bool CMusicDatabase::GetSongsByPath(const std::string& strPath1, + MAPSONGS& songmap, + bool bAppendToMap) +{ + std::string strPath(strPath1); + try + { + if (!URIUtils::HasSlashAtEnd(strPath)) + URIUtils::AddSlashAtEnd(strPath); + + if (!bAppendToMap) + songmap.clear(); + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Filename is not unique for a path as songs from a cuesheet have same filename. + // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet + // in a folder and some edited and rescanned. + // Hence order by filename so these songs can be gathered together. + std::string strSQL = PrepareSQL("SELECT * FROM songview " + "WHERE strPath='%s' ORDER BY strFileName", + strPath.c_str()); + if (!m_pDS->query(strSQL)) + return false; + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return false; + } + + // Each file is potentially mapped to a list of songs, gather these and save as list + VECSONGS songs; + std::string filename; + while (!m_pDS->eof()) + { + CSong song = GetSongFromDataset(); + if (!filename.empty() && filename != song.strFileName) + { + // Save songs for previous filename + songmap.insert(std::make_pair(filename, songs)); + songs.clear(); + } + filename = song.strFileName; + songs.emplace_back(song); + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath); + } + + return false; +} + +void CMusicDatabase::EmptyCache() +{ + m_genreCache.erase(m_genreCache.begin(), m_genreCache.end()); + m_pathCache.erase(m_pathCache.begin(), m_pathCache.end()); +} + +bool CMusicDatabase::Search(const std::string& search, CFileItemList& items) +{ + auto start = std::chrono::steady_clock::now(); + // first grab all the artists that match + SearchArtists(search, items); + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + CLog::Log(LOGDEBUG, "{} Artist search in {} ms", __FUNCTION__, duration.count()); + + start = std::chrono::steady_clock::now(); + // then albums that match + SearchAlbums(search, items); + end = std::chrono::steady_clock::now(); + duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + CLog::Log(LOGDEBUG, "{} Album search in {} ms", __FUNCTION__, duration.count()); + + start = std::chrono::steady_clock::now(); + // and finally songs + SearchSongs(search, items); + end = std::chrono::steady_clock::now(); + duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + CLog::Log(LOGDEBUG, "{} Songs search in {} ms", __FUNCTION__, duration.count()); + + return true; +} + +bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CMusicDbUrl baseUrl; + if (!baseUrl.FromString("musicdb://songs/")) + return false; + + std::string strSQL; + if (search.size() >= MIN_FULL_SEARCH_LENGTH) + strSQL = PrepareSQL("SELECT * FROM songview " + "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000", + search.c_str(), search.c_str()); + else + strSQL = PrepareSQL("SELECT * FROM songview " + "WHERE strTitle LIKE '%s%%' LIMIT 1000", + search.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + return false; + + while (!m_pDS->eof()) + { + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(item.get(), baseUrl); + items.Add(item); + m_pDS->next(); + } + + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList& albums) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + if (search.size() >= MIN_FULL_SEARCH_LENGTH) + strSQL = PrepareSQL("SELECT * FROM albumview " + "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'", + search.c_str(), search.c_str()); + else + strSQL = PrepareSQL("SELECT * FROM albumview " + "WHERE strAlbum LIKE '%s%%'", + search.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + + const std::string& albumLabel(g_localizeStrings.Get(558)); // Album + while (!m_pDS->eof()) + { + CAlbum album = GetAlbumFromDataset(m_pDS.get()); + std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum); + CFileItemPtr pItem(new CFileItem(path, album)); + std::string label = StringUtils::Format("[{}] {}", albumLabel, album.strAlbum); + pItem->SetLabel(label); + // sort label is stored in the title tag + label = StringUtils::Format("B {}", album.strAlbum); + pItem->GetMusicInfoTag()->SetTitle(label); + albums.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::CleanupSongsByIds(const std::string& strSongIds) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + // ok, now find all idSong's + std::string strSQL = PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath " + "WHERE song.idSong IN %s", + strSongIds.c_str()); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + std::vector<std::string> songsToDelete; + while (!m_pDS->eof()) + { // get the full song path + std::string strFileName = URIUtils::AddFileToFolder( + m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString()); + + // Special case for streams inside an audio decoder package file. + // The last dir in the path is the audio file that + // contains the stream, so test if its there + if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName), + KODI_ADDON_AUDIODECODER_TRACK_EXT)) + { + strFileName = URIUtils::GetDirectory(strFileName); + // we are dropping back to a file, so remove the slash at end + URIUtils::RemoveSlashAtEnd(strFileName); + } + + if (!CFile::Exists(strFileName, false)) + { // file no longer exists, so add to deletion list + songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString()); + } + m_pDS->next(); + } + m_pDS->close(); + + if (!songsToDelete.empty()) + { + std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")"; + // ok, now delete these songs + all references to them from the linked tables + strSQL = "delete from song where idSong in " + strSongsToDelete; + m_pDS->exec(strSQL); + m_pDS->close(); + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()"); + } + return false; +} + +bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/) +{ + try + { + int total; + // Count total number of songs + total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS); + // No songs to clean + if (total == 0) + return true; + + // run through all songs and get all unique path ids + int iLIMIT = 1000; + for (int i = 0;; i += iLIMIT) + { + std::string strSQL = PrepareSQL("SELECT song.idSong FROM song " + "ORDER BY song.idSong LIMIT %i OFFSET %i", + iLIMIT, i); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + // keep going until no rows are left! + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + std::vector<std::string> songIds; + while (!m_pDS->eof()) + { + songIds.push_back(m_pDS->fv("song.idSong").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")"; + CLog::Log(LOGDEBUG, "Checking songs from song ID list: {}", strSongIds); + if (progressDialog) + { + int percentage = i * 100 / total; + if (percentage > progressDialog->GetPercentage()) + { + progressDialog->SetPercentage(percentage); + progressDialog->Progress(); + } + if (progressDialog->IsCanceled()) + { + m_pDS->close(); + return false; + } + } + if (!CleanupSongsByIds(strSongIds)) + return false; + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()"); + } + return false; +} + +bool CMusicDatabase::CleanupAlbums() +{ + try + { + // This must be run AFTER songs have been cleaned up + // delete albums with no reference to songs + std::string strSQL = "SELECT * FROM album " + "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)"; + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + std::vector<std::string> albumIds; + while (!m_pDS->eof()) + { + albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + + std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")"; + // ok, now we can delete them and the references in the linked tables + strSQL = "delete from album where idAlbum in " + strAlbumIds; + m_pDS->exec(strSQL); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()"); + } + return false; +} + +bool CMusicDatabase::CleanupPaths() +{ + try + { + // needs to be done AFTER the songs and albums have been cleaned up. + // we can happily delete any path that has no reference to a song + // but we must keep all paths that have been scanned that may contain songs in subpaths + + // first create a temporary table of song paths + m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n"); + m_pDS->exec("INSERT INTO songpaths " + "SELECT idPath, strPath FROM path " + "WHERE idPath IN (SELECT idPath FROM song)\n"); + + // grab all paths that aren't immediately connected with a song + std::string sql = "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)"; + if (!m_pDS->query(sql)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + // and construct a list to delete + std::vector<std::string> pathIds; + while (!m_pDS->eof()) + { + // anything that isn't a parent path of a song path is to be deleted + std::string path = m_pDS->fv("strPath").get_asString(); + sql = PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'", + StringUtils::utf8_strlen(path.c_str()), path.c_str()); + if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0) + pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete + m_pDS2->close(); + m_pDS->next(); + } + m_pDS->close(); + + if (!pathIds.empty()) + { + // do the deletion, and drop our temp table + std::string deleteSQL = + "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")"; + m_pDS->exec(deleteSQL); + } + m_pDS->exec("drop table songpaths"); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted"); + } + return false; +} + +bool CMusicDatabase::InsideScannedPath(const std::string& path) +{ + std::string sql = PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1", + path.size(), path.c_str()); + return !GetSingleValue(sql).empty(); +} + +bool CMusicDatabase::CleanupArtists() +{ + try + { + // (nested queries by Bobbin007) + // must be executed AFTER the song, album and their artist link tables are cleaned. + // Don't delete [Missing] the missing artist tag artist + + // Create temp table to avoid 1442 trigger hell on mysql + m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)"); + m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist"); + m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist"); + m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID)); + // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large. + // To avoid MySQL hanging or timeout create a table of unique ids with primary key + m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)"); + m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists"); + m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)"); + // Tidy up temp tables + m_pDS->exec("DROP TABLE tmp_delartists"); + m_pDS->exec("DROP TABLE tmp_keep"); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted"); + } + return false; +} + +bool CMusicDatabase::CleanupGenres() +{ + try + { + // Cleanup orphaned song genres (ie those that don't belong to a song entry) + // (nested queries by Bobbin007) + // Must be executed AFTER the song, and song_genre have been cleaned. + std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)"; + m_pDS->exec(strSQL); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted"); + } + return false; +} + +bool CMusicDatabase::CleanupInfoSettings() +{ + try + { + // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry) + // Must be executed AFTER the album and artist tables have been cleaned. + std::string strSQL = "DELETE FROM infosetting " + "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) " + "AND idSetting NOT IN (SELECT idInfoSetting FROM album)"; + m_pDS->exec(strSQL); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted"); + } + return false; +} + +bool CMusicDatabase::CleanupRoles() +{ + try + { + // Cleanup orphaned roles (ie those that don't belong to a song entry) + // Must be executed AFTER the song, and song_artist tables have been cleaned. + // Do not remove default role (ROLE_ARTIST) + std::string strSQL = "DELETE FROM role " + "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)"; + m_pDS->exec(strSQL); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted"); + } + return false; +} + +bool CMusicDatabase::DeleteRemovedLinks() +{ + try + { + std::string strSQL = "DELETE FROM removed_link"; + m_pDS->exec(strSQL); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks"); + } + return false; +} + +bool CMusicDatabase::CleanupOrphanedItems() +{ + // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath() + // remove_links not cleared here - done in CheckArtistLinksChanged() + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + SetLibraryLastUpdated(); + if (!CleanupAlbums()) + return false; + if (!CleanupArtists()) + return false; + if (!CleanupGenres()) + return false; + if (!CleanupRoles()) + return false; + if (!CleanupInfoSettings()) + return false; + return true; +} + +int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/) +{ + if (nullptr == m_pDB) + return ERROR_DATABASE; + if (nullptr == m_pDS) + return ERROR_DATABASE; + + int ret; + std::chrono::seconds duration; + auto time = std::chrono::steady_clock::now(); + CLog::Log(LOGINFO, "{}: Starting musicdatabase cleanup ..", __FUNCTION__); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted"); + + SetLibraryLastCleaned(); + + // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link + // Check that triggers actually exist first as interrupting the clean causes them to not be + // re-created + + m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteSongArtist"); + m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteAlbumArtist"); + + // first cleanup any songs with invalid paths + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{318}); + progressDialog->SetLine(2, CVariant{330}); + progressDialog->SetPercentage(0); + progressDialog->Progress(); + } + if (!CleanupSongs(progressDialog)) + { + ret = ERROR_REORG_SONGS; + goto error; + } + // then the albums that are not linked to a song or to album, or whose path is removed + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{326}); + progressDialog->SetPercentage(20); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + ret = ERROR_CANCEL; + goto error; + } + } + if (!CleanupAlbums()) + { + ret = ERROR_REORG_ALBUM; + goto error; + } + // now the paths + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{324}); + progressDialog->SetPercentage(40); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + ret = ERROR_CANCEL; + goto error; + } + } + if (!CleanupPaths()) + { + ret = ERROR_REORG_PATH; + goto error; + } + // and finally artists + genres + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{320}); + progressDialog->SetPercentage(60); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + ret = ERROR_CANCEL; + goto error; + } + } + if (!CleanupArtists()) + { + ret = ERROR_REORG_ARTIST; + goto error; + } + //Genres, roles and info settings progress in one step + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{322}); + progressDialog->SetPercentage(80); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + ret = ERROR_CANCEL; + goto error; + } + } + if (!CleanupGenres()) + { + ret = ERROR_REORG_OTHER; + goto error; + } + if (!CleanupRoles()) + { + ret = ERROR_REORG_OTHER; + goto error; + } + if (!CleanupInfoSettings()) + { + ret = ERROR_REORG_OTHER; + goto error; + } + if (!DeleteRemovedLinks()) + { + ret = ERROR_REORG_OTHER; + goto error; + } + + // commit transaction + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{328}); + progressDialog->SetPercentage(90); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + ret = ERROR_CANCEL; + goto error; + } + } + if (!CommitTransaction()) + { + ret = ERROR_WRITING_CHANGES; + goto error; + } + + // Recreate DELETE triggers on song_artist and album_artist + CreateRemovedLinkTriggers(); + + // and compress the database + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{331}); + progressDialog->SetPercentage(100); + progressDialog->Close(); + } + + duration = + std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time); + CLog::Log(LOGINFO, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__, + duration.count()); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished"); + + if (!Compress(false)) + { + return ERROR_COMPRESSING; + } + return ERROR_OK; + +error: + RollbackTransaction(); + // Recreate DELETE triggers on song_artist and album_artist + CreateRemovedLinkTriggers(); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished"); + return ret; +} + +bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space) +{ + if (strImage.length() > space) + { + strImage = strImage.substr(0, space); + // Tidy to last </thumb> tag + size_t iPos = strImage.rfind("</thumb>"); + if (iPos == std::string::npos) + return false; + strImage = strImage.substr(0, iPos + 8); + } + return true; +} + +bool CMusicDatabase::LookupCDDBInfo(bool bRequery /*=false*/) +{ +#ifdef HAS_DVD_DRIVE + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_AUDIOCDS_USECDDB)) + return false; + + // check network connectivity + if (!CServiceBroker::GetNetwork().IsAvailable()) + return false; + + // Get information for the inserted disc + CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(); + if (pCdInfo == NULL) + return false; + + // If the disc has no tracks, we are finished here. + int nTracks = pCdInfo->GetTrackCount(); + if (nTracks <= 0) + return false; + + // Delete old info if any + if (bRequery) + { + std::string strFile = StringUtils::Format("{:x}.cddb", pCdInfo->GetCddbDiscId()); + CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile)); + } + + // Prepare cddb + Xcddb cddb; + cddb.setCacheDir(m_profileManager.GetCDDBFolder()); + + // Do we have to look for cddb information + if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo)) + { + CGUIDialogProgress* pDialogProgress = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + CGUIDialogSelect* pDlgSelect = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + + if (!pDialogProgress) + return false; + if (!pDlgSelect) + return false; + + // Show progress dialog if we have to connect to freedb.org + pDialogProgress->SetHeading(CVariant{255}); //CDDB + pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info + pDialogProgress->SetLine(1, CVariant{256}); + pDialogProgress->SetLine(2, CVariant{""}); + pDialogProgress->ShowProgressBar(false); + pDialogProgress->Open(); + + // get cddb information + if (!cddb.queryCDinfo(pCdInfo)) + { + pDialogProgress->Close(); + int lasterror = cddb.getLastError(); + + // Have we found more then on match in cddb for this disc,... + if (lasterror == E_WAIT_FOR_INPUT) + { + // ...yes, show the matches found in a select dialog + // and let the user choose an entry. + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{255}); + int i = 1; + while (true) + { + std::string strTitle = cddb.getInexactTitle(i); + if (strTitle == "") + break; + + const std::string& strArtist = cddb.getInexactArtist(i); + if (!strArtist.empty()) + strTitle += " - " + strArtist; + + pDlgSelect->Add(strTitle); + i++; + } + pDlgSelect->Open(); + + // Has the user selected a match... + int iSelectedCD = pDlgSelect->GetSelectedItem(); + if (iSelectedCD >= 0) + { + // ...query cddb for the inexact match + if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD)) + pCdInfo->SetNoCDDBInfo(); + } + else + pCdInfo->SetNoCDDBInfo(); + } + else if (lasterror == E_NO_MATCH_FOUND) + { + pCdInfo->SetNoCDDBInfo(); + } + else + { + pCdInfo->SetNoCDDBInfo(); + // ..no, an error occurred, display it to the user + std::string strErrorText = + StringUtils::Format("[{}] {}", cddb.getLastError(), cddb.getLastErrorText()); + HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)}, + CVariant{0}); + } + } // if ( !cddb.queryCDinfo( pCdInfo ) ) + else + pDialogProgress->Close(); + } + + // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA + + return pCdInfo->HasCDDBInfo(); +#else + return false; +#endif +} + +void CMusicDatabase::DeleteCDDBInfo() +{ +#ifdef HAS_DVD_DRIVE + CFileItemList items; + if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb", + DIR_FLAG_NO_FILE_DIRS)) + { + HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426}); + return; + } + // Show a selectdialog that the user can select the album to delete + CGUIDialogSelect* pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (pDlg) + { + pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)}); + pDlg->Reset(); + + std::map<uint32_t, std::string> mapCDDBIds; + for (int i = 0; i < items.Size(); ++i) + { + if (items[i]->m_bIsFolder) + continue; + + std::string strFile = URIUtils::GetFileName(items[i]->GetPath()); + strFile.erase(strFile.size() - 5, 5); + uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16); + Xcddb cddb; + cddb.setCacheDir(m_profileManager.GetCDDBFolder()); + + if (!cddb.queryCache(lDiscId)) + continue; + + std::string strDiskTitle, strDiskArtist; + cddb.getDiskTitle(strDiskTitle); + cddb.getDiskArtist(strDiskArtist); + + std::string str; + if (strDiskArtist.empty()) + str = strDiskTitle; + else + str = strDiskTitle + " - " + strDiskArtist; + + pDlg->Add(str); + mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str)); + } + + pDlg->Sort(); + pDlg->Open(); + + // and wait till user selects one + int iSelectedAlbum = pDlg->GetSelectedItem(); + if (iSelectedAlbum < 0) + { + mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end()); + return; + } + + std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel(); + for (const auto& i : mapCDDBIds) + { + if (i.second == strSelectedAlbum) + { + std::string strFile = StringUtils::Format("{:x}.cddb", (unsigned int)i.first); + CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile)); + break; + } + } + mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end()); + } +#endif +} + +void CMusicDatabase::Clean() +{ + // If we are scanning for music info in the background, + // other writing access to the database is prohibited. + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057}); + return; + } + + if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES) + { + CMusicDatabase musicdatabase; + if (musicdatabase.Open()) + { + int iReturnString = musicdatabase.Cleanup(); + musicdatabase.Close(); + + if (iReturnString != ERROR_OK) + { + HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString}); + } + } + } +} + +bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // get primary genres for songs - could be simplified to just SELECT * FROM genre? + std::string strSQL = "SELECT %s FROM genre "; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // if there are extra WHERE conditions we might need access + // to songview or albumview for these conditions + if (!extFilter.where.empty()) + { + if (extFilter.where.find("artistview") != std::string::npos) + { + extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre"); + extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong"); + extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong"); + extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist"); + } + else if (extFilter.where.find("songview") != std::string::npos) + { + extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre"); + extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong"); + } + else if (extFilter.where.find("albumview") != std::string::npos) + { + extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre"); + extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong"); + extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum"); + } + extFilter.AppendGroup("genre.idGenre"); + } + extFilter.AppendWhere("genre.strGenre != ''"); + + if (countOnly) + { + extFilter.fields = "COUNT(DISTINCT genre.idGenre)"; + extFilter.group.clear(); + extFilter.order.clear(); + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 + ? extFilter.fields.c_str() + : "genre.*") + + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + // get data from returned rows + while (!m_pDS->eof()) + { + CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString())); + pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString()); + pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre"); + + CMusicDbUrl itemUrl = musicUrl; + std::string strDir = StringUtils::Format("{}/", m_pDS->fv("genre.idGenre").get_asInt()); + itemUrl.AppendPath(strDir); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /*= Filter()*/, + bool countOnly /*= false*/) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Get sources for selection list when add/edit filter or smartplaylist rule + std::string strSQL = "SELECT %s FROM source "; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // if there are extra WHERE conditions we might need access + // to songview or albumview for these conditions + if (!extFilter.where.empty()) + { + if (extFilter.where.find("artistview") != std::string::npos) + { + extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource"); + extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum"); + extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist"); + } + else if (extFilter.where.find("songview") != std::string::npos) + { + extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource"); + extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum"); + } + else if (extFilter.where.find("albumview") != std::string::npos) + { + extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource"); + extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum"); + } + extFilter.AppendGroup("source.idSource"); + } + else + { // Get only sources that have been scanned into music library + extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource"); + extFilter.AppendGroup("source.idSource"); + } + + if (countOnly) + { + extFilter.fields = "COUNT(DISTINCT source.idSource)"; + extFilter.group.clear(); + extFilter.order.clear(); + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 + ? extFilter.fields.c_str() + : "source.*") + + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + // get data from returned rows + while (!m_pDS->eof()) + { + CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString())); + pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString()); + pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source"); + + CMusicDbUrl itemUrl = musicUrl; + std::string strDir = StringUtils::Format("{}/", m_pDS->fv("source.idSource").get_asInt()); + itemUrl.AppendPath(strDir); + itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt()); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting; + std::string strSQL; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE); + + useOriginalYears = + useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/"); + + if (!useOriginalYears) + { // Get years from year part of release date + strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview "; + extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)"); + } + else + { // Get years from year part of original date + strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview "; + extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)"); + } + if (!BuildSQL(strSQL, extFilter, strSQL)) + return false; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // get data from returned rows + while (!m_pDS->eof()) + { + CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString())); + pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt()); + if (useOriginalYears) + pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear"); + else + pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year"); + + CMusicDbUrl itemUrl = musicUrl; + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + itemUrl.AppendPath(strDir); + if (useOriginalYears) + itemUrl.AddOption("useoriginalyear", true); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // get roles with artists having that role + std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role " + "JOIN song_artist ON song_artist.idRole = role.idRole "; + + if (!BuildSQL(strSQL, extFilter, strSQL)) + return false; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // get data from returned rows + while (!m_pDS->eof()) + { + std::string labelValue = m_pDS->fv("role.strRole").get_asString(); + CFileItemPtr pItem(new CFileItem(labelValue)); + pItem->GetMusicInfoTag()->SetTitle(labelValue); + pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role"); + CMusicDbUrl itemUrl = musicUrl; + std::string strDir = StringUtils::Format("{}/", m_pDS->fv("role.idRole").get_asInt()); + itemUrl.AppendPath(strDir); + itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt()); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + musicUrl.AddOption("year", year); + musicUrl.AddOption("show_singles", true); // allow singles to be listed + + Filter filter; + return GetAlbumsByWhere(musicUrl.ToString(), filter, items); +} + +bool CMusicDatabase::GetCommonNav(const std::string& strBaseDir, + const std::string& table, + const std::string& labelField, + CFileItemList& items, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (table.empty() || labelField.empty()) + return false; + + try + { + Filter extFilter = filter; + std::string strSQL = "SELECT %s FROM " + table + " "; + extFilter.AppendGroup(labelField); + extFilter.AppendWhere(labelField + " != ''"); + + if (countOnly) + { + extFilter.fields = "COUNT(DISTINCT " + labelField + ")"; + extFilter.group.clear(); + extFilter.order.clear(); + } + + // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format + // e.g. LIKE '%symphony%' would be taken as a %s format argument + strSQL = PrepareSQL(strSQL, + !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str()); + + CMusicDbUrl musicUrl; + if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl)) + return false; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound <= 0) + { + m_pDS->close(); + return false; + } + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + // get data from returned rows + while (!m_pDS->eof()) + { + std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString(); + CFileItemPtr pItem(new CFileItem(labelValue)); + + CMusicDbUrl itemUrl = musicUrl; + std::string strDir = StringUtils::Format("{}/", labelValue); + itemUrl.AppendPath(strDir); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + + m_pDS->next(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return false; +} + +bool CMusicDatabase::GetAlbumTypesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly); +} + +bool CMusicDatabase::GetMusicLabelsNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly); +} + +bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir, + CFileItemList& items, + bool albumArtistsOnly /* = false */, + int idGenre /* = -1 */, + int idAlbum /* = -1 */, + int idSong /* = -1 */, + const Filter& filter /* = Filter() */, + const SortDescription& sortDescription /* = SortDescription() */, + bool countOnly /* = false */) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + try + { + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + if (idGenre > 0) + musicUrl.AddOption("genreid", idGenre); + else if (idAlbum > 0) + musicUrl.AddOption("albumid", idAlbum); + else if (idSong > 0) + musicUrl.AddOption("songid", idSong); + + // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS) + // when local option already present in music URL thus allowing it to be an option in custom nodes + if (!musicUrl.HasOption("albumartistsonly")) + musicUrl.AddOption("albumartistsonly", albumArtistsOnly); + + bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly); + + return result; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetArtistsByWhere( + const std::string& strBaseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription /* = SortDescription() */, + bool countOnly /* = false */) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + auto start = std::chrono::steady_clock::now(); + int total = -1; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + bool extended = false; + bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0); + + // if there are extra WHERE conditions (from media filter dialog) we might + // need access to songview or albumview for these conditions + if (!extFilter.where.empty()) + { + if (extFilter.where.find("songview") != std::string::npos) + { + extended = true; + extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist " + "JOIN songview ON songview.idSong = song_artist.idSong"); + } + else if (extFilter.where.find("albumview") != std::string::npos) + { + extended = true; + extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist " + "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum"); + } + if (extended) + extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count number of artsits that satisfy selection criteria (no limit built) + // Count done in full query fetch when unlimited + if (countOnly || limitedInSQL) + { + if (extended) + { + // Count distinct without group by + Filter countFilter = extFilter; + countFilter.group.clear(); + std::string strSQLWhere; + if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere)) + return false; + total = GetSingleValueInt( + "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS); + } + else + total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS); + } + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", total); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + // Apply any limiting directly in SQL and so sort as well + if (limitedInSQL) + { + extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart); + } + + // Apply sort in SQL + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sorting.sortAttributes = + static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName); + // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query + GetOrderFilter(MediaTypeArtist, sorting, extFilter); + + strSQLExtra.clear(); + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + std::string strSQL; + std::string strFields = "artistview.*"; + if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0) + strFields = "artistview.*, " + extFilter.fields; + strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + auto queryStart = std::chrono::steady_clock::now(); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + auto queryEnd = std::chrono::steady_clock::now(); + auto queryDuration = + std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart); + + // Store the total number of artists as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + DatabaseResults results; + results.reserve(iRowsFound); + // Populate results field vector from dataset + FieldList fields; + if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results)) + return false; + // Store item list sort order + items.SetSortMethod(sortDescription.sortBy); + items.SetSortOrder(sortDescription.sortOrder); + + // Get Artists from returned rows + items.Reserve(results.size()); + const dbiplus::query_data& data = m_pDS->get_result_set().records; + for (const auto& i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + try + { + CArtist artist = GetArtistFromDataset(record, false); + CFileItemPtr pItem(new CFileItem(artist)); + + CMusicDbUrl itemUrl = musicUrl; + std::string path = StringUtils::Format("{}/", artist.idArtist); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist); + // Set icon now to avoid slow per item processing in FillInDefaultIcon later + pItem->SetProperty("icon_never_overlay", true); + pItem->SetArt("icon", "DefaultArtist.png"); + + SetPropertiesFromArtist(*pItem, artist); + items.Add(pItem); + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__, + items.Size()); + } + } + // cleanup + m_pDS->close(); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1} ms query took {2} ms", + __FUNCTION__, duration.count(), queryDuration.count()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum& album) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = PrepareSQL("SELECT albumview.* FROM song " + "JOIN albumview on song.idAlbum = albumview.idAlbum " + "WHERE song.idSong='%i'", + idSong); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return false; + } + + album = GetAlbumFromDataset(m_pDS.get()); + + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir, + CFileItemList& items, + int idGenre /* = -1 */, + int idArtist /* = -1 */, + const Filter& filter /* = Filter() */, + const SortDescription& sortDescription /* = SortDescription() */, + bool countOnly /* = false */) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + // where clause + if (idGenre > 0) + musicUrl.AddOption("genreid", idGenre); + + if (idArtist > 0) + musicUrl.AddOption("artistid", idArtist); + + return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly); +} + +bool CMusicDatabase::GetAlbumsByWhere( + const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription /* = SortDescription() */, + bool countOnly /* = false */) +{ + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + try + { + auto start = std::chrono::steady_clock::now(); + int total = -1; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + bool extended = false; + bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0); + + // If there are extra WHERE conditions (from media filter dialog) we might + // need access to songview for these conditions + if (extFilter.where.find("songview") != std::string::npos) + { + extended = true; + extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum"); + extFilter.AppendGroup("albumview.idAlbum"); + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count number of albums that satisfy selection criteria (no limit built) + // Count done in full query fetch when unlimited + if (countOnly || limitedInSQL) + { + if (extended) + { + // Count distinct without group by + Filter countFilter = extFilter; + countFilter.group.clear(); + std::string strSQLWhere; + if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere)) + return false; + total = GetSingleValueInt( + "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS); + } + else + total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS); + } + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", total); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + // Apply any limiting directly in SQL + if (limitedInSQL) + { + extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart); + } + + // Apply sort in SQL + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sorting.sortAttributes = + static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName); + // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query + GetOrderFilter(MediaTypeAlbum, sorting, extFilter); + // Modify order to use correct calculated year field + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)"); + else + StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)"); + + strSQLExtra.clear(); + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + std::string strSQL; + std::string strFields = "albumview.*"; + if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0) + strFields = "albumview.*, " + extFilter.fields; + strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + auto querytime = std::chrono::steady_clock::now(); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + auto queryEnd = std::chrono::steady_clock::now(); + auto queryDuration = + std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - querytime); + + // Store the total number of albums as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + DatabaseResults results; + results.reserve(iRowsFound); + // Populate results field vector from dataset + FieldList fields; + if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results)) + return false; + // Store item list sort order + items.SetSortMethod(sorting.sortBy); + items.SetSortOrder(sorting.sortOrder); + + // Get albums from returned rows + items.Reserve(results.size()); + const dbiplus::query_data& data = m_pDS->get_result_set().records; + for (const auto& i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + try + { + CMusicDbUrl itemUrl = musicUrl; + std::string path = StringUtils::Format("{}/", record->at(album_idAlbum).get_asInt()); + itemUrl.AppendPath(path); + + CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record))); + // Set icon now to avoid slow per item processing in FillInDefaultIcon later + pItem->SetProperty("icon_never_overlay", true); + pItem->SetArt("icon", "DefaultAlbumCover.png"); + items.Add(pItem); + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__, + items.Size()); + } + } + // cleanup + m_pDS->close(); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__, + duration.count(), queryDuration.count()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where); + } + return false; +} + +bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir, + CFileItemList& items, + int idAlbum, + const Filter& filter, + const SortDescription& sortDescription, + bool countOnly) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + if (idAlbum > 0) + musicUrl.AddOption("albumid", idAlbum); + + return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly); +} + +bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription, + bool countOnly) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(baseDir)) + return false; + return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly); +} + +bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription, + bool countOnly) +{ + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + try + { + auto start = std::chrono::steady_clock::now(); + int total = -1; + std::string strSQL; + + Filter extFilter = filter; + SortDescription sorting = sortDescription; + + if (!GetFilter(musicUrl, extFilter, sorting)) + return false; + + extFilter.AppendGroup("albumview.idAlbum, iDisc"); + + // If there are extra songview WHERE conditions adjust to song or albumview + // fields, and join Path table for strPath + // ! @todo: convert songview fields into to song or albumview fields + // But not sure we ever get songview fields in filter - REMOVE?? + if (extFilter.where.find("songview.strPath") != std::string::npos) + { + extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath"); + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Apply any limiting directly in SQL if there is either no special sorting or random sort + // When limited, random sort is also applied in SQL + bool limitedInSQL = extFilter.limit.empty() && + (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) && + (sorting.limitStart > 0 || sorting.limitEnd > 0); + + if (countOnly || limitedInSQL) + { + // Count number of discs that satisfy selection criteria + // (when fetching all records get total from row count of results dataset) + // Count not allow for same non-null title discs to be grouped together + strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = " + "albumview.idAlbum " + + strSQLExtra; + strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc "; + total = GetSingleValueInt(strSQL, m_pDS); + } + if (countOnly) + { + items.SetProperty("total", total); + return true; + } + // Apply limits and random sort order directly in SQL + if (limitedInSQL) + { + if (sorting.sortBy == SortByRandom) + strSQLExtra += PrepareSQL(" ORDER BY RANDOM()"); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + else + strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc"); + + strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* " + "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " + + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + auto queryStart = std::chrono::steady_clock::now(); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + auto queryEnd = std::chrono::steady_clock::now(); + auto queryDuration = + std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + DatabaseResults results; + results.reserve(iRowsFound); + + // Avoid sorting with limits, just fetch results from dataset + // Limit when SortByNone already applied in SQL, + // Need guaranteed ordering for dataset processing to group by disc title + // so apply sort later to fileitems list rather than dataset + sorting.sortBy = SortByNone; + if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results)) + return false; + + // Get data from returned rows, note possibly multiple albums although usually only one + items.Reserve(total); + int albumOffset = 2; + CAlbum album; + bool useTitle = true; // Assume we want to match by disc title later unless we have no titles + std::string oldDiscTitle; + const dbiplus::query_data& data = m_pDS->get_result_set().records; + for (const auto& i : results) + { + unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger()); + const dbiplus::sql_record* const record = data.at(targetRow); + try + { + if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt()) + { // New album + useTitle = true; + album = GetAlbumFromDataset(record, albumOffset); + } + + int discnum = record->at(0).get_asInt(); + std::string strDiscSubtitle = record->at(1).get_asString(); + if (strDiscSubtitle.empty()) + { // Make (fake) disc title from disc number, group by disc number as no real title to match + strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum); + useTitle = false; + } + else if (oldDiscTitle == strDiscSubtitle) + { // disc title already added to list, fetch the next disc + continue; + } + oldDiscTitle = strDiscSubtitle; + + CMusicDbUrl itemUrl = musicUrl; + std::string path = StringUtils::Format("{}/", discnum); + itemUrl.AppendPath(path); + + // When disc titles are provided group discs together by title not number. + // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a + if (useTitle) + itemUrl.AddOption("disctitle", strDiscSubtitle.c_str()); + else + itemUrl.AddOption("discid", discnum); + CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album)); + pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order?? + pItem->GetMusicInfoTag()->SetDiscNumber(discnum); + pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle); + pItem->SetLabel(strDiscSubtitle); + // Set icon now to avoid slow per item processing in FillInDefaultIcon later + pItem->SetProperty("icon_never_overlay", true); + pItem->SetArt("icon", "DefaultAlbumCover.png"); + items.Add(pItem); + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__, + items.Size()); + } + } + + // cleanup + m_pDS->close(); + + // Finally do any sorting in items list we have not been able to do before in SQL or dataset, + // that is when have join with songartistview and sorting other than random with limit + if (sorting.sortBy != SortByNone && !(limitedInSQL && sorting.sortBy == SortByRandom)) + items.Sort(sorting); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__, + duration.count(), queryDuration.count()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where); + } + + return false; +} +int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */) +{ + int iDiscTotal = -1; + CFileItemList itemscount; + if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true)) + iDiscTotal = itemscount.GetProperty("total").asInteger32(); + return iDiscTotal; +} + +bool CMusicDatabase::GetSongsFullByWhere( + const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription /* = SortDescription() */, + bool artistData /* = false*/) +{ + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + try + { + auto start = std::chrono::steady_clock::now(); + int total = -1; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + bool extended = false; + bool limitedInSQL = + extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0); + + // If there are extra WHERE conditions (from media filter dialog) we might + // need access to albumview for these conditions + if (extFilter.where.find("albumview") != std::string::npos) + { + extended = true; + extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum"); + } + + // Build songview <where> for count + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count (without group by) number of songs that satisfy selection criteria + // Much quicker to use song table, not songview, when filtering only on song fields + if (extended || + (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos || + extFilter.where.find("strPath") != std::string::npos || + extFilter.where.find("bCompilation") != std::string::npos || + extFilter.where.find("bBoxedset") != std::string::npos))) + total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS); + else + { + std::string strSQLsong = strSQLExtra; + StringUtils::Replace(strSQLsong, "songview", "song"); + total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS); + } + + if (extended) + extFilter.AppendGroup("songview.idSong"); + + // Apply any limiting directly in SQL + if (limitedInSQL) + { + extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart); + } + + // Apply sort in SQL + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sorting.sortAttributes = + static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName); + // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query + GetOrderFilter(MediaTypeSong, sorting, extFilter); + // Modify order to use correct calculated year field + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)"); + else + StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)"); + + std::string strFields = "songview.*"; + if (!artistData || limitedInSQL) + { + // Build songview <where> + <order by> + <limits> + strSQLExtra.clear(); + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + } + else + strFields = "songview.*, songartistview.*"; + if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0) + strFields = strFields + ", " + extFilter.fields; + + std::string strSQL; + if (artistData) + { // Get data from song and song_artist tables to fully populate songs with artists + // All songs now have at least one artist so inner join sufficient + // Build songartistview JOIN part of query + Filter joinFilter; + std::string strSQLJoin; + joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong"); + if (sortDescription.sortBy == SortByRandom) + joinFilter.AppendOrder("songartistview.idSong"); + else + joinFilter.order = extFilter.order; + if (limitedInSQL) + { + StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong"); + StringUtils::Replace(joinFilter.order, "songview.", "sv."); + } + else + joinFilter.where = extFilter.where; + joinFilter.AppendOrder("songartistview.idRole"); + joinFilter.AppendOrder("songartistview.iOrder"); + if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin)) + return false; + + if (limitedInSQL) + { + // When have artist data (all roles) and LIMIT on songs use inline view + // SELECT sv.*, songartistview.* FROM + // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv + // <order by sv fields>, songartistview.idRole, songartistview.iOrder + // Apply where clause, limits and order to songview, then join to songartistview this gives + // multiple records per song in result set + strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra; + strSQL = "(" + strSQL + ") AS sv "; + strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin; + } + else + strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin; + } + else + strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra; + + CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL); + auto queryStart = std::chrono::steady_clock::now(); + // run query + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + auto queryEnd = std::chrono::steady_clock::now(); + auto queryDuration = + std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart); + + // Store the total number of songs as a property + items.SetProperty("total", total); + + DatabaseResults results; + results.reserve(iRowsFound); + // Populate results field vector from dataset + FieldList fields; + if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results)) + return false; + // Store item list sort order + items.SetSortMethod(sorting.sortBy); + items.SetSortOrder(sorting.sortOrder); + + // Get songs from returned rows. If join songartistview then there is a row for every artist + items.Reserve(total); + int songArtistOffset = song_enumCount; + int songId = -1; + VECARTISTCREDITS artistCredits; + const dbiplus::query_data& data = m_pDS->get_result_set().records; + int count = 0; + for (const auto& i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + try + { + if (songId != record->at(song_idSong).get_asInt()) + { //New song + if (songId > 0 && !artistCredits.empty()) + { + //Store artist credits for previous song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + songId = record->at(song_idSong).get_asInt(); + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(record, item.get(), musicUrl); + // HACK for sorting by database returned order + item->m_iprogramCount = ++count; + // Set icon now to avoid slow per item processing in FillInDefaultIcon later + item->SetProperty("icon_never_overlay", true); + item->SetArt("icon", "DefaultAudio.png"); + items.Add(item); + } + // Get song artist credits and contributors + if (artistData) + { + int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt(); + if (idSongArtistRole == ROLE_ARTIST) + artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset)); + else + items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole( + GetArtistRoleFromDataset(record, songArtistOffset)); + } + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where); + return (items.Size() > 0); + } + } + if (!artistCredits.empty()) + { + //Store artist credits for final song + GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get()); + artistCredits.clear(); + } + // cleanup + m_pDS->close(); + + // Ensure random order of item list when results set sorted by idSong for artist processing + // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node + // navigation. Order is read later from view state and list sorting is then triggered by + // CGUIMediaWindow::Update in both cases. + // So sorting here is currently redundant, but the consistent place to do it. + // !@ todo: do sorting once, preferably in SQL + if (sorting.sortBy == SortByRandom && artistData) + items.Sort(sorting); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__, + duration.count(), queryDuration.count()); + + return true; + } + catch (...) + { + // cleanup + m_pDS->close(); + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where); + } + return false; +} + +bool CMusicDatabase::GetSongsByWhere( + const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription /* = SortDescription() */) +{ + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + try + { + int total = -1; + + std::string strSQL = "SELECT %s FROM songview "; + + Filter extFilter = filter; + CMusicDbUrl musicUrl; + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // if there are extra WHERE conditions we might need access + // to songview for these conditions + if (extFilter.where.find("albumview") != std::string::npos) + { + extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum"); + extFilter.AppendGroup("songview.idSong"); + } + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0)) + { + total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0 + ? filter.fields.c_str() + : "songview.*") + + strSQLExtra; + + CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL); + // run query + if (!m_pDS->query(strSQL)) + return false; + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + DatabaseResults results; + results.reserve(iRowsFound); + if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results)) + return false; + + // get data from returned rows + items.Reserve(results.size()); + const dbiplus::query_data& data = m_pDS->get_result_set().records; + int count = 0; + for (const auto& i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + try + { + CFileItemPtr item(new CFileItem); + GetFileItemFromDataset(record, item.get(), musicUrl); + // HACK for sorting by database returned order + item->m_iprogramCount = ++count; + items.Add(item); + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where); + return (items.Size() > 0); + } + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + // cleanup + m_pDS->close(); + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where); + } + return false; +} + +bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(baseDir)) + return false; + + musicUrl.AddOption("year", year); + + Filter filter; + return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true); +} + +bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir, + CFileItemList& items, + int idGenre, + int idArtist, + int idAlbum, + const SortDescription& sortDescription /* = SortDescription() */) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + if (idAlbum > 0) + musicUrl.AddOption("albumid", idAlbum); + + if (idGenre > 0) + musicUrl.AddOption("genreid", idGenre); + + if (idArtist > 0) + musicUrl.AddOption("artistid", idArtist); + + Filter filter; + return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true); +} + +// clang-format off +typedef struct +{ + std::string fieldJSON; // Field name in JSON schema + std::string formatJSON; // Format in JSON schema + bool bSimple; // Fetch field directly to JSON output + std::string fieldDB; // Name of field in db query + std::string SQL; // SQL for scalar subqueries or field alias +} translateJSONField; + +static const translateJSONField JSONtoDBArtist[] = { + // Table and single value join fields + { "artist", "string", true, "strArtist", "" }, // Label field at top + { "sortname", "string", true, "strSortname", "" }, + { "instrument", "array", true, "strInstruments", "" }, + { "description", "string", true, "strBiography", "" }, + { "genre", "array", true, "strGenres", "" }, + { "mood", "array", true, "strMoods", "" }, + { "style", "array", true, "strStyles", "" }, + { "yearsactive", "array", true, "strYearsActive", "" }, + { "born", "string", true, "strBorn", "" }, + { "formed", "string", true, "strFormed", "" }, + { "died", "string", true, "strDied", "" }, + { "disbanded", "string", true, "strDisbanded", "" }, + { "type", "string", true, "strType", "" }, + { "gender", "string", true, "strGender", "" }, + { "disambiguation", "string", true, "strDisambiguation", "" }, + { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element + { "dateadded", "string", true, "dateAdded", "" }, + { "datenew", "string", true, "dateNew", "" }, + { "datemodified", "string", true, "dateModified", "" }, + + // JOIN fields (multivalue), same order as _JoinToArtistFields + { "", "", false, "isSong", "" }, + { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" }, + { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" }, + { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" }, + { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" }, + { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" }, + { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" }, + { "art", "", false, "idArt", "art.art_id AS idArt" }, + { "", "", false, "artType", "art.type AS artType" }, + { "", "", false, "artURL", "art.url AS artURL" }, + { "", "", false, "idRole", "song_artist.idRole" }, + { "roles", "", false, "strRole", "role.strRole" }, + { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" }, + // Derived from joined tables + { "isalbumartist", "bool", false, "", "" }, + { "thumbnail", "string", false, "", "" }, + { "fanart", "string", false, "", "" } + /* + Sources and genre are related via album, and so the dataset only contains source and genre + pairs that exist, rather than all the genres being repeated for every source. We can not only + look at genres for the first source, and genre can be out of order. + */ +}; +// clang-format on + +static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField); + +bool CMusicDatabase::GetArtistsByWhereJSON( + const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription /* = SortDescription() */) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + total = -1; + + size_t resultcount = 0; + Filter extFilter; + CMusicDbUrl musicUrl; + SortDescription sorting = sortDescription; + //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // Replace view names in filter with table names + StringUtils::Replace(extFilter.where, "artistview", "artist"); + StringUtils::Replace(extFilter.where, "albumview", "album"); + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count number of artists that satisfy selection criteria + //(includes xsp limits from filter, but not sort limits) + total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS); + resultcount = static_cast<size_t>(total); + + // Process albumartistsonly option + const CUrlOptions::UrlOptions& options = musicUrl.GetOptions(); + bool albumArtistsOnly(false); + auto option = options.find("albumartistsonly"); + if (option != options.end()) + albumArtistsOnly = option->second.asBoolean(); + // Process role options + int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles. + option = options.find("roleid"); + if (option != options.end()) + roleidfilter = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("role"); + if (option != options.end()) + { + if (option->second.asString() == "all" || option->second.asString() == "%") + roleidfilter = -1000; //All roles + else + roleidfilter = GetRoleByName(option->second.asString()); + } + } + + // Get order by (and any scalar query artist fields) + int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter); + // Replace artistview field names in order by artist table field names + StringUtils::Replace(extFilter.order, "artistview", "artist"); + StringUtils::Replace(extFilter.fields, "artistview", "artist"); + + // Grab and adjust artist sort field that may have been added to filter + // These need to be added to the end of the artist table field list + std::string artistsortSQL = extFilter.fields; + extFilter.fields.clear(); + + std::string strSQL; + + // Setup fields to query, and album field number mapping + // Find first join field (isSong) in JSONtoDBArtist for offset + int index_firstjoin = -1; + for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++) + { + if (JSONtoDBArtist[i].fieldDB == "isSong") + { + index_firstjoin = i; + break; + } + } + Filter joinFilter; + Filter albumArtistFilter; + Filter songArtistFilter; + DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount)); + extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON + std::vector<int> dbfieldindex; + // JSON "label" field is strArtist which is also output as "artist", query field once output twice + extFilter.AppendField(JSONtoDBArtist[0].fieldDB); + dbfieldindex.emplace_back(0); // Output "artist" + + // Check each optional artist db field that could be retrieved (not "artist") + for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++) + { + bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end(); + if (JSONtoDBArtist[i].bSimple) + { + // Check for non-join fields in order too. + // Query these in inline view (but not output) so can ref in outer order + bool foundOrderby(false); + if (!foundJSON) + foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos; + if (foundOrderby || foundJSON) + { + // Store indexes of requested artist table and scalar subquery fields + // to be output, and -1 when not output to JSON + if (!foundJSON) + dbfieldindex.emplace_back(-1); + else + dbfieldindex.emplace_back(i); + // Field from scaler subquery + if (!JSONtoDBArtist[i].SQL.empty()) + extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL)); + else + // Field from artist table + extFilter.AppendField(JSONtoDBArtist[i].fieldDB); + } + } + else if (foundJSON) + // Field from join or derived from joined fields + joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true); + } + + // Append calculated artistsort field that may have been added to filter + // Field used only for ORDER BY, not output to JSON + extFilter.AppendField(artistsortSQL); + for (int i = 0; i < iAddedFields; i++) + dbfieldindex.emplace_back(-2); // columns in dataset + + // Build JOIN, WHERE, ORDER BY and LIMIT for inline view + strSQLExtra = ""; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Add any LIMIT clause to strSQLExtra + if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0)) + { + strSQLExtra += + DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart); + resultcount = std::min( + DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart), + resultcount); + } + + // Setup multivalue JOINs, GROUP BY and ORDER BY + bool bJoinAlbumArtist(false); + bool bJoinSongArtist(false); + if (sortDescription.sortBy != SortByRandom) + { + // Repeat inline view order (that always includes idArtist) on join query + std::string order = extFilter.order; + StringUtils::Replace(order, "artist.", "a1."); + joinFilter.AppendOrder(order); + } + else + joinFilter.AppendOrder("a1.idArtist"); + joinFilter.AppendGroup("a1.idArtist"); + // Album artists and song artists + if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) || + joinLayout.GetFetch(joinToArtist_idSourceAlbum) || + joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) || + joinLayout.GetFetch(joinToArtist_strRole)) + { + bJoinAlbumArtist = true; + albumArtistFilter.AppendField("album_artist.idArtist AS id"); + if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole)) + { + bJoinSongArtist = true; + songArtistFilter.AppendField("song_artist.idArtist AS id"); + songArtistFilter.AppendField("1 AS isSong"); + albumArtistFilter.AppendField("0 AS isSong"); + joinLayout.SetField(joinToArtist_isSong, + JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB); + joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB); + joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB); + } + } + else if (joinLayout.GetFetch(joinToArtist_isalbumartist)) + { + // Filtering album artists only and isalbumartist requested but not source, songgenres or roles, + // so no need for join to album_artist table. Set fetching fetch false so that + // joinLayout.HasFilterFields() is false + joinLayout.SetFetch(joinToArtist_isalbumartist, false); + } + + // Sources + if (joinLayout.GetFetch(joinToArtist_idSourceAlbum)) + { // Left join as source may have been removed but leaving lib entries + albumArtistFilter.AppendJoin( + "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum"); + albumArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL); + joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB); + joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB); + if (bJoinSongArtist) + { + songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong"); + songArtistFilter.AppendJoin( + "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum"); + songArtistFilter.AppendField( + "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB); + songArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL); + albumArtistFilter.AppendField( + "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB); + joinLayout.SetField(joinToArtist_idSourceSong, + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB); + joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB); + joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB); + } + else + { + joinLayout.SetField(joinToArtist_idSourceAlbum, + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true); + } + } + + // Songgenres - id and genres always both + if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum)) + { // All albums have songs, but left join genre as songs may not have genre + albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum"); + albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong"); + albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre"); + albumArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL); + albumArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL); + joinLayout.SetField(joinToArtist_strSongGenreAlbum, + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB); + joinFilter.AppendGroup( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB); + joinFilter.AppendOrder( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB); + if (bJoinSongArtist) + { // Left join genre as songs may not have genre + songArtistFilter.AppendJoin( + "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong"); + songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre"); + songArtistFilter.AppendField( + "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB); + songArtistFilter.AppendField( + "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB); + songArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL); + songArtistFilter.AppendField( + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL); + albumArtistFilter.AppendField( + "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB); + albumArtistFilter.AppendField( + "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB); + joinLayout.SetField(joinToArtist_idSongGenreSong, + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB); + joinLayout.SetField( + joinToArtist_strSongGenreSong, + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB); + joinFilter.AppendGroup( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB); + joinFilter.AppendOrder( + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB); + } + else + { // Define field alias names in join layout + joinLayout.SetField(joinToArtist_idSongGenreAlbum, + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL, + true); + joinLayout.SetField(joinToArtist_strSongGenreAlbum, + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL); + } + } + + // Roles + if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole)) + // Only looking at album and song artists not other roles (default), + // so filter dataset rows likewise. + songArtistFilter.AppendWhere("song_artist.idRole = 1"); + else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field + (bJoinSongArtist && (joinLayout.GetFetch(joinToArtist_idSourceAlbum) || + joinLayout.GetFetch(joinToArtist_idSongGenreAlbum)))) + { // Rows from many roles so fetch roleid for "roles", source and genre processing + songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL); + // Add fake column to album_artist query + albumArtistFilter.AppendField("-1 AS " + + JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB); + joinLayout.SetField(joinToArtist_idRole, + JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB); + joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB); + joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB); + } + if (joinLayout.GetFetch(joinToArtist_strRole)) + { // Fetch role desc + songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole"); + songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL); + // Add fake column to album_artist query + albumArtistFilter.AppendField("'albumartist' AS " + + JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB); + } + + // Build source, genre and roles part of query + if (bJoinAlbumArtist) + { + if (bJoinSongArtist) + { + // Combine song and album artist filter as UNION and add to join filter as an inline view + std::string strAlbumSQL; + if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL)) + return false; + strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL; + std::string strSongSQL; + if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL)) + return false; + strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL; + + joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL + + ") AS albumSong ON id = a1.idArtist"); + } + else + { //Only join album_artist, so move filter elements to join filter + joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist"); + joinFilter.AppendJoin(albumArtistFilter.join); + } + } + + //Art + bool bJoinArt(false); + bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) || + joinLayout.GetOutput(joinToArtist_thumbnail) || + joinLayout.GetOutput(joinToArtist_fanart); + if (bJoinArt) + { // Left join as artist may not have any art + joinFilter.AppendJoin( + "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'"); + joinLayout.SetField(joinToArtist_idArt, + JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL, + joinLayout.GetOutput(joinToArtist_idArt)); + joinLayout.SetField(joinToArtist_artType, + JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL); + joinLayout.SetField(joinToArtist_artURL, + JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL); + joinFilter.AppendGroup("art.art_id"); + joinFilter.AppendOrder("arttype"); + if (!joinLayout.GetOutput(joinToArtist_idArt)) + { + if (!joinLayout.GetOutput(joinToArtist_thumbnail)) + // Fanart only + joinFilter.AppendJoin("AND art.type = 'fanart'"); + else if (!joinLayout.GetOutput(joinToArtist_fanart)) + // Thumb only + joinFilter.AppendJoin("AND art.type = 'thumb'"); + } + } + else if (bJoinSongArtist) + joinFilter.group.clear(); // UNION only so no GROUP BY needed + + // Build JOIN part of query (if we have one) + std::string strSQLJoin; + if (joinLayout.HasFilterFields()) + if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin)) + return false; + + // Adjust where in the results record the join fields are allowing for the + // inline view fields (Quicker than finding field by name every time) + // idArtist + other artist fields + joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size())); + + // Build full query + // When have multiple value joins e.g. song genres, use inline view + // SELECT a1.*, <join fields> FROM + // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1 + // <joins> <group by> <order by> + <joins order by> + // Don't use prepareSQL - confuses arttype = 'thumb' filter + + strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra; + if (joinLayout.HasFilterFields()) + { + strSQL = "(" + strSQL + ") AS a1 "; + strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin; + } + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + // run query + auto start = std::chrono::steady_clock::now(); + + if (!m_pDS->query(strSQL)) + return false; + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count()); + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound <= 0) + { + m_pDS->close(); + return true; + } + + // Get artists from returned rows. Joins means there can be many rows per artist + int artistId = -1; + int sourceId = -1; + int genreId = -1; + int roleId = -1; + int artId = -1; + std::vector<int> genreidlist; + std::vector<int> sourceidlist; + std::vector<int> roleidlist; + bool bArtDone(false); + bool bHaveArtist(false); + bool bIsAlbumArtist(true); + bool bGenreFoundViaAlbum(false); + CVariant artistObj; + result["artists"].reserve(resultcount); + while (!m_pDS->eof() || bHaveArtist) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (m_pDS->eof() || artistId != record->at(0).get_asInt()) + { + // Store previous or last artist + if (bHaveArtist) + { + // Convert any empty MBid array into an array with one empty element [""] + // to match the number of artist ID (way other mbid arrays handled) + if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty()) + artistObj["musicbrainzartistid"].append(""); + + result["artists"].append(artistObj); + bHaveArtist = false; + artistObj.clear(); + } + if (artistObj.empty()) + { + // Initialise fields, ensure those with possible null values are set to correct empty variant type + if (joinLayout.GetOutput(joinToArtist_idSourceAlbum)) + artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray); + if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum)) + artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray); + if (joinLayout.GetOutput(joinToArtist_idArt)) + artistObj["art"] = CVariant(CVariant::VariantTypeObject); + if (joinLayout.GetOutput(joinToArtist_thumbnail)) + artistObj["thumbnail"] = ""; + if (joinLayout.GetOutput(joinToArtist_fanart)) + artistObj["fanart"] = ""; + + sourceId = -1; + roleId = -1; + genreId = -1; + artId = -1; + genreidlist.clear(); + bGenreFoundViaAlbum = false; + sourceidlist.clear(); + roleidlist.clear(); + bArtDone = false; + } + if (m_pDS->eof()) + continue; // Having saved the last artist stop + + // New artist + artistId = record->at(0).get_asInt(); + bHaveArtist = true; + artistObj["artistid"] = artistId; + artistObj["label"] = record->at(1).get_asString(); + artistObj["artist"] = record->at(1).get_asString(); // Always have "artist" + bIsAlbumArtist = true; //Album artist by default + if (joinLayout.GetOutput(joinToArtist_isalbumartist)) + { + // Not album artist when fetching song artists too and first row for artist isSong=true + if (bJoinSongArtist) + bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool(); + artistObj["isalbumartist"] = bIsAlbumArtist; + } + for (size_t i = 0; i < dbfieldindex.size(); i++) + if (dbfieldindex[i] > -1) + { + if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer") + artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt(); + else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float") + artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = + record->at(1 + i).get_asFloat(); + else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array") + artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = StringUtils::Split( + record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_musicItemSeparator); + else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean") + artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool(); + else + artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = + record->at(1 + i).get_asString(); + } + } + if (bJoinAlbumArtist) + { + bool bAlbumArtistRow(true); + int idRoleRow = -1; + if (bJoinSongArtist) + { + bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool(); + if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 && + !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull()) + { + idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt(); + } + } + + // Sources - gathered via both album_artist and song_artist (with role = 1) + if (joinLayout.GetFetch(joinToArtist_idSourceAlbum)) + { + if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 && + !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() && + sourceId != + record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) || + (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 && + !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() && + sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt())) + { + bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats + bool found(false); + sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt(); + if (!bAlbumArtistRow) + { + // Skip other roles (when fetching them) + if (idRoleRow > 1) + { + found = true; + } + else + { + sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt(); + // Song artist row may repeat sources found via album artist + // Already have that source? + for (const auto& i : sourceidlist) + if (i == sourceId) + { + found = true; + break; + } + } + } + if (!found) + { + sourceidlist.emplace_back(sourceId); + artistObj["sourceid"].append(sourceId); + } + } + } + // Songgenres - via album artist takes precedence + /* + Sources and genre are related via album, and so the dataset only contains source + and genre pairs that exist, rather than all the genres being repeated for every + source. We can not only look at genres for the first source, and genre can be + found out of order. + Also song artist row may repeat genres found via album artist + */ + if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum)) + { + std::string strGenre; + bool newgenre(false); + if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 && + !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() && + genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt()) + { + bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats + newgenre = true; + genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt(); + strGenre = + record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString(); + } + else if (!bAlbumArtistRow && !bGenreFoundViaAlbum && + joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 && + !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() && + genreId != + record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt()) + { + bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats + newgenre = idRoleRow <= 1; // Skip other roles (when fetching them) + genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt(); + strGenre = + record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString(); + } + if (newgenre) + { + // Already have that genre? + bool found(false); + for (const auto& i : genreidlist) + if (i == genreId) + { + found = true; + break; + } + if (!found) + { + bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow; + genreidlist.emplace_back(genreId); + CVariant genreObj; + genreObj["genreid"] = genreId; + genreObj["title"] = strGenre; + artistObj["songgenres"].append(genreObj); + } + } + } + // Roles - gathered via song_artist roleid rows + if (joinLayout.GetFetch(joinToArtist_idRole)) + { + if (!bAlbumArtistRow && roleId != idRoleRow) + { + bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats + roleId = idRoleRow; + if (joinLayout.GetOutput(joinToArtist_strRole)) + { + // Already have that role? + bool found(false); + for (const auto& i : roleidlist) + if (i == roleId) + { + found = true; + break; + } + if (!found) + { + roleidlist.emplace_back(roleId); + CVariant roleObj; + roleObj["roleid"] = roleId; + roleObj["role"] = + record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString(); + artistObj["roles"].append(roleObj); + } + } + } + } + } + // Art + if (bJoinArt && !bArtDone && + !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() && + record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 && + artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt()) + { + artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt(); + if (joinLayout.GetOutput(joinToArtist_idArt)) + { + artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] = + CTextureUtils::GetWrappedImageURL( + record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString()); + } + if (joinLayout.GetOutput(joinToArtist_thumbnail) && + record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb") + { + artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL( + record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString()); + } + if (joinLayout.GetOutput(joinToArtist_fanart) && + record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart") + { + artistObj["fanart"] = CTextureUtils::GetWrappedImageURL( + record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString()); + } + } + + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + + // Ensure random order of output when results set is sorted to process multi-value joins + if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields()) + KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +// clang-format off +static const translateJSONField JSONtoDBAlbum[] = { + // albumview (inc scalar subquery fields use in filter rules) + { "title", "string", true, "strAlbum", "" }, // Label field at top + { "description", "string", true, "strReview", "" }, + { "genre", "array", true, "strGenres", "" }, + { "theme", "array", true, "strThemes", "" }, + { "mood", "array", true, "strMoods", "" }, + { "style", "array", true, "strStyles", "" }, + { "type", "string", true, "strType", "" }, + { "albumlabel", "string", true, "strLabel", "" }, + { "rating", "float", true, "fRating", "" }, + { "votes", "integer", true, "iVotes", "" }, + { "userrating", "unsigned", true, "iUserrating", "" }, + { "isboxset", "boolean", true, "bBoxedSet", "" }, + { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" }, + { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table + { "compilation", "boolean", true, "bCompilation", "" }, + { "releasetype", "string", true, "strReleaseType", "" }, + { "totaldiscs", "integer", true, "iDiscTotal", "" }, + { "sortartist", "string", true, "strArtistSort", "" }, + { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" }, + { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view + { "dateadded", "string", true, "dateAdded", "" }, + { "datenew", "string", true, "dateNew", "" }, + { "datemodified", "string", true, "dateModified", "" }, + { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view + { "originaldate", "string", true, "strOrigReleaseDate", "" }, + { "releasedate", "string", true, "strReleaseDate", "" }, + { "albumstatus", "string", true, "strReleaseStatus", "" }, + { "albumduration", "integer", true, "iAlbumDuration", "" }, + // Scalar subquery fields + { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate + { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" }, + { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song " + "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } , + // Single value JOIN fields + { "thumbnail", "image", true, "thumbnail", "art.url AS thumbnail" }, // or (SELECT art.url FROM art WHERE art.media_id = album.idAlbum AND art.media_type = "album" AND art.type = "thumb") as url + // JOIN fields (multivalue), same order as _JoinToAlbumFields + { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" }, + { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" }, + { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" }, + /* + Album "fanart" and "art" fields of JSON schema are fetched using thumbloader + and separate queries to allow for fallback strategy. + + Using albmview, rather than album table, as view has scalar subqueries for + playcount and lastplayed already defined. Needed as MySQL does + not support use of scalar subquery field alias names in where clauses (they + have to be repeated) and these fields can be used by filter rules. + Using this view is no slower than the album table as these scalar fields are + only calculated (slowing query) when field is in field list. + */ +}; +// clang-format on + +static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField); + +bool CMusicDatabase::GetAlbumsByWhereJSON( + const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription /* = SortDescription() */) +{ + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + total = -1; + + size_t resultcount = 0; + Filter extFilter; + CMusicDbUrl musicUrl; + // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription + // passed in at the start of the function + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // Replace view names in filter with table names + StringUtils::Replace(extFilter.where, "artistview", "artist"); + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count number of albums that satisfy selection criteria + // (includes xsp limits from filter, but not sort limits) + // Use albumview as filter rules in where clause may use scalar query fields + total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS); + resultcount = static_cast<size_t>(total); + + // Get order by (and any scalar query artist fields + int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter); + + // Grab calculated artist/title sort fields that may have been added to filter + // These need to be added to the end of the album table field list + std::string calcsortfieldsSQL = extFilter.fields; + extFilter.fields.clear(); + + std::string strSQL; + + // Setup fields to query, and album field number mapping + // Find idArtist in JSONtoDBAlbum, offset of first join field + int index_idArtist = -1; + for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++) + { + if (JSONtoDBAlbum[i].fieldDB == "idArtist") + { + index_idArtist = i; + break; + } + } + Filter joinFilter; + DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount)); + extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON + std::vector<int> dbfieldindex; + // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice + extFilter.AppendField(JSONtoDBAlbum[0].fieldDB); + if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end()) + dbfieldindex.emplace_back(0); // Output "title" + else + dbfieldindex.emplace_back(-1); // fetch but not output + + // Check each optional album db field that could be retrieved (not label) + for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++) + { + bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end(); + if (JSONtoDBAlbum[i].bSimple) + { + // Check for non-join fields in order too. + // Query these in inline view (but not output) so can ref in outer order + bool foundOrderby(false); + if (!foundJSON) + foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos; + if (foundOrderby || foundJSON) + { + // Store indexes of requested album table and scalar subquery fields + // to be output, and -1 when not output to JSON + if (!foundJSON) + dbfieldindex.emplace_back(-1); + else + dbfieldindex.emplace_back(i); + if (!JSONtoDBAlbum[i].SQL.empty()) + // Field from scaler subquery + extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL)); + else + // Field from album table + extFilter.AppendField(JSONtoDBAlbum[i].fieldDB); + } + } + else if (foundJSON) + // Field from join found in JSON request + joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true); + } + + // Append calculated artist/title sort fields that may have been added to filter + // Field used only for ORDER BY, not output to JSON + extFilter.AppendField(calcsortfieldsSQL); + for (int i = 0; i < iAddedFields; i++) + dbfieldindex.emplace_back(-1); // columns in dataset + + // JOIN art tables if needed (fields output and/or in sort) + if (extFilter.fields.find("art.") != std::string::npos) + { // Left join as not all albums have art, but only have one thumb at most + extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum " + "AND art.media_type = 'album' AND art.type = 'thumb'"); + } + + // Build JOIN, WHERE, ORDER BY and LIMIT for inline view + strSQLExtra = ""; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Add any LIMIT clause to strSQLExtra + if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0)) + { + strSQLExtra += + DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart); + resultcount = std::min( + DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart), + resultcount); + } + + // Setup multivalue JOINs, GROUP BY and ORDER BY + bool bJoinAlbumArtist(false); + if (sortDescription.sortBy != SortByRandom) + { + // Repeat inline view order (that always includes idAlbum) on join query + std::string order = extFilter.order; + StringUtils::Replace(order, "albumview.", "a1."); + joinFilter.AppendOrder(order); + } + else + joinFilter.AppendOrder("a1.idAlbum"); + joinFilter.AppendGroup("a1.idAlbum"); + // Album artists + if (joinLayout.GetFetch(joinToAlbum_idArtist) || joinLayout.GetFetch(joinToAlbum_strArtist) || + joinLayout.GetFetch(joinToAlbum_strArtistMBID)) + { // All albums have at least one artist so inner join sufficient + bJoinAlbumArtist = true; + joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum"); + joinFilter.AppendGroup("album_artist.idArtist"); + joinFilter.AppendOrder("album_artist.iOrder"); + // Ensure idArtist is queried + if (!joinLayout.GetFetch(joinToAlbum_idArtist)) + joinLayout.SetField(joinToAlbum_idArtist, + JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL); + } + // artist table needed for strArtist or MBID + // (album_artist.strArtist can be an alias or spelling variation) + if (joinLayout.GetFetch(joinToAlbum_strArtist) || + joinLayout.GetFetch(joinToAlbum_strArtistMBID)) + joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist"); + + // Build JOIN part of query (if we have one) + std::string strSQLJoin; + if (joinLayout.HasFilterFields()) + if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin)) + return false; + + // Adjust where in the results record the join fields are allowing for the + // inline view fields (Quicker than finding field by name every time) + // idAlbum + other album fields + joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size())); + + // Build full query + // When have multiple value joins (artists or song genres) use inline view + // SELECT a1.*, <join fields> FROM + // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1 + // <joins> <group by> <order by> <joins order by> + // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator + + strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra; + if (joinLayout.HasFilterFields()) + { + strSQL = "(" + strSQL + ") AS a1 "; + strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin; + } + + // Modify query to use correct year field + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate"); + else + StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate"); + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + // run query + auto start = std::chrono::steady_clock::now(); + + if (!m_pDS->query(strSQL)) + return false; + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count()); + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound <= 0) + { + m_pDS->close(); + return true; + } + + // Get albums from returned rows. Joins means there can be many rows per album + int albumId = -1; + int artistId = -1; + CVariant albumObj; + result["albums"].reserve(resultcount); + while (!m_pDS->eof() || !albumObj.empty()) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (m_pDS->eof() || albumId != record->at(0).get_asInt()) + { + // Store previous or last album + if (!albumObj.empty()) + { + // Split sources string into int array + if (albumObj.isMember("sourceid")) + { + std::vector<std::string> sources = + StringUtils::Split(albumObj["sourceid"].asString(), ";"); + albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray); + for (size_t i = 0; i < sources.size(); i++) + albumObj["sourceid"].append(atoi(sources[i].c_str())); + } + result["albums"].append(albumObj); + albumObj.clear(); + artistId = -1; + } + if (m_pDS->eof()) + continue; // Having saved last album stop + + // New album + albumId = record->at(0).get_asInt(); + albumObj["albumid"] = albumId; + albumObj["label"] = record->at(1).get_asString(); + for (size_t i = 0; i < dbfieldindex.size(); i++) + if (dbfieldindex[i] > -1) + { + if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres") + { + // Convert "20,Jazz,54,New Age,65,Rock" into array of objects + std::vector<std::string> values = + StringUtils::Split(record->at(1 + i).get_asString(), ","); + if (values.size() % 2 == 0) // Must contain an even number of entries + { + for (size_t j = 0; j + 1 < values.size(); j += 2) + { + int idGenre = atoi(values[j].c_str()); + if (idGenre > 0) + { + CVariant genreObj; + genreObj["genreid"] = idGenre; + genreObj["title"] = values[j + 1]; + albumObj["songgenres"].append(genreObj); + } + } + } + // Ensure albums with null songgenres get empty array + if (!albumObj.isMember("songgenres")) + albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray); + } + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer") + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt(); + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned") + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = + std::max(record->at(1 + i).get_asInt(), 0); + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float") + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = + std::max(record->at(1 + i).get_asFloat(), 0.f); + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array") + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split( + record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_musicItemSeparator); + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean") + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool(); + else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image") + { + std::string url = record->at(1 + i).get_asString(); + if (!url.empty()) + url = CTextureUtils::GetWrappedImageURL(url); + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url; + } + else + albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString(); + } + } + if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1) + { + if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt()) + { + artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt(); + if (joinLayout.GetOutput(joinToAlbum_idArtist)) + albumObj["artistid"].append(artistId); + if (artistId == BLANKARTIST_ID) + { + if (joinLayout.GetOutput(joinToAlbum_strArtist)) + albumObj["artist"].append(StringUtils::Empty); + if (joinLayout.GetOutput(joinToAlbum_strArtistMBID)) + albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty); + } + else + { + if (joinLayout.GetOutput(joinToAlbum_strArtist) && + joinLayout.GetRecNo(joinToAlbum_strArtist) > -1) + albumObj["artist"].append( + record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString()); + if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) && + joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1) + albumObj["musicbrainzalbumartistid"].append( + record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString()); + } + } + } + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + + // Ensure random order of output when results set is sorted to process multi-value joins + if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields()) + KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +// clang-format off +static const translateJSONField JSONtoDBSong[] = { + // table and single value join fields + { "title", "string", true, "strTitle", "" }, // Label field at top + { "albumid", "integer", true, "song.idAlbum", "" }, + { "", "", true, "song.iTrack", "" }, + { "displayartist", "string", true, "song.strArtistDisp", "" }, + { "sortartist", "string", true, "song.strArtistSort", "" }, + { "genre", "array", true, "song.strGenres", "" }, + { "duration", "integer", true, "iDuration", "" }, + { "comment", "string", true, "comment", "" }, + { "", "string", true, "strFileName", "" }, + { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" }, + { "playcount", "integer", true, "iTimesPlayed", "" }, + { "lastplayed", "string", true, "lastPlayed", "" }, + { "rating", "float", true, "rating", "" }, + { "votes", "integer", true, "votes", "" }, + { "userrating", "unsigned", true, "song.userrating", "" }, + { "mood", "array", true, "mood", "" }, + { "dateadded", "string", true, "song.dateAdded", "" }, + { "datenew", "string", true, "song.dateNew", "" }, + { "datemodified", "string", true, "song.dateModified", "" }, + { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" }, + { "", "string", true, "strPath", "path.strPath AS strPath" }, + { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" }, + { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" }, + { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" }, + { "disctitle", "string", true, "song.strDiscSubtitle", "" }, + { "bpm", "integer", true, "iBPM", "" }, + { "originaldate", "string" , true, "song.strOrigReleaseDate","" }, + { "releasedate", "string" , true, "song.strReleaseDate", "" }, + { "bitrate", "integer", true, "iBitRate", "" }, + { "samplerate", "integer", true, "iSampleRate", "" }, + { "channels", "integer", true, "iChannels", "" }, + + // JOIN fields (multivalue), same order as _JoinToSongFields + { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" }, + { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" }, + { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" }, + { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" }, + { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" }, + { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" }, + { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" }, + { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" }, + { "", "", false, "idRole", "song_artist.idRole" }, + { "", "", false, "strRole", "role.strRole" }, + { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" }, + { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order + { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" }, + + { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" }, + { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" }, + { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" }, + { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" }, + { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" }, + + // Scalar subquery fields + { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate + { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" }, + { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" }, + { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" }, + /* + Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using + thumbloader and separate queries to allow for fallback strategy + "lyrics"?? Can be set for an item (by addons) but not held in db so + AudioLibrary.GetSongs() never fills this field despite being in schema + + FROM ( SELECT * FROM song + JOIN album ON album.idAlbum = song.idAlbum + JOIN path ON path.idPath = song.idPath) AS sv + JOIN album_artist ON album_artist.idAlbum = song.idAlbum + JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist + JOIN song_artist ON song_artist.idSong = song.idSong + JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist + JOIN role ON song_artist.idRole = role.idRole + LEFT JOIN song_genre ON song.idSong = song_genre.idSong + + */ +}; +// clang-format on + +static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField); + +bool CMusicDatabase::GetSongsByWhereJSON( + const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription /* = SortDescription() */) +{ + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + total = -1; + + size_t resultcount = 0; + Filter extFilter; + CMusicDbUrl musicUrl; + // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription + // passed into the function + SortDescription sorting = sortDescription; + if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + // Replace view names in filter with table names + StringUtils::Replace(extFilter.where, "artistview", "artist"); + StringUtils::Replace(extFilter.where, "albumview", "album"); + StringUtils::Replace(extFilter.where, "songview.strPath", "strPath"); + StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum"); + StringUtils::Replace(extFilter.where, "songview", "song"); + StringUtils::Replace(extFilter.where, "songartistview", "song_artist"); + + // JOIN album and path tables needed by filter rules in where clause + if (extFilter.where.find("album.") != std::string::npos || + extFilter.where.find("strAlbum") != std::string::npos) + { // All songs have one album so inner join sufficient + extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum"); + } + if (extFilter.where.find("strPath") != std::string::npos) + { // All songs have one path so inner join sufficient + extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath"); + } + + // Build JOINs and WHERE needed by filter for counting songs + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Count number of songs that satisfy selection criteria + // (includes xsp limits from filter, but not sort limits) + total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS); + resultcount = static_cast<size_t>(total); + + int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter); + // Replace songview field names in order by with song, album path table field names + // Field names in album same as song: + // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation + StringUtils::Replace(extFilter.order, "songview.strPath", "strPath"); + StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum"); + StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation"); + StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp"); + StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp"); + StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort"); + StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType"); + StringUtils::Replace(extFilter.order, "songview", "song"); + StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort"); + StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp"); + StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum"); + StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle"); + + // Grab calculated artist/title sort fields that may have been added to filter + // These need to be added to the end of the song table field list + std::string calcsortfieldsSQL = extFilter.fields; + extFilter.fields.clear(); + + std::string strSQL; + + // Setup fields to query, and song field number mapping + // Find idAlbumArtist in JSONtoDBSong, offset of first join field + int index_idAlbumArtist = -1; + for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++) + { + if (JSONtoDBSong[i].fieldDB == "idAlbumArtist") + { + index_idAlbumArtist = i; + break; + } + } + Filter joinFilter; + DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount)); + extFilter.AppendField("song.idSong"); // ID "songid" in JSON + std::vector<int> dbfieldindex; + // JSON "label" field is strTitle which may also be requested as "title", query field once output twice + extFilter.AppendField(JSONtoDBSong[0].fieldDB); + if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end()) + dbfieldindex.emplace_back(0); // Output "title" + else + dbfieldindex.emplace_back(-1); // Fetch but not output + std::vector<std::string> rolefieldlist; + std::vector<int> roleidlist; + // Check each optional db field that could be retrieved (not label) + for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++) + { + bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end(); + if (JSONtoDBSong[i].bSimple) + { + // Check for non-join fields in order too. + // Query these in inline view (but not output) so can ref in outer order + bool foundOrderby(false); + if (!foundJSON) + foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos; + if (foundOrderby || foundJSON) + { + // Store indexes of requested album table and scalar subquery fields + // to be output, and -1 when not output to JSON + if (!foundJSON) + dbfieldindex.emplace_back(-1); + else + dbfieldindex.emplace_back(i); + if (!JSONtoDBSong[i].SQL.empty()) + // Field from scaler subquery + extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL)); + else + // Field from song table + extFilter.AppendField(JSONtoDBSong[i].fieldDB); + } + } + else if (foundJSON) + { // Field from join found in JSON request + if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_")) + { + joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true); + } + else + { // "contributors", "displaycomposer" etc. + rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON); + } + } + } + // Append calculated artist/title sort fields that may have been added to filter + // Field used only for ORDER BY, not output to JSON + extFilter.AppendField(calcsortfieldsSQL); + for (int i = 0; i < iAddedFields; i++) + dbfieldindex.emplace_back(-1); // columns in dataset + + // Build matching list of role id for "displaycomposer", "displayconductor", + // "displayorchestra", "displaylyricist" + if (!rolefieldlist.empty()) + { + for (const auto& name : rolefieldlist) + { + int idRole = -1; + if (StringUtils::StartsWith(name, "display")) + idRole = GetRoleByName(name.substr(7)); + roleidlist.emplace_back(idRole); + } + } + + // JOIN album and path tables needed for field output and/or in sort + // if not already there for filter + if ((extFilter.fields.find("album.") != std::string::npos || + extFilter.fields.find("strAlbum") != std::string::npos) && + extFilter.join.find("JOIN album") == std::string::npos) + { // All songs have one album so inner join sufficient + extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum"); + } + if (extFilter.fields.find("path.") != std::string::npos && + extFilter.join.find("JOIN path") == std::string::npos) + { // All songs have one path so inner join sufficient + extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath"); + } + + // Build JOIN, WHERE, ORDER BY and LIMIT for inline view + strSQLExtra = ""; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Add any LIMIT clause to strSQLExtra + if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0)) + { + strSQLExtra += + DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart); + resultcount = std::min( + DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart), + resultcount); + } + + // Setup multivalue JOINs, GROUP BY and ORDER BY + bool bJoinSongArtist(false); + bool bJoinAlbumArtist(false); + bool bJoinRole(false); + if (sortDescription.sortBy != SortByRandom) + { + // Repeat inline view order (that always includes idSong) on join query + std::string order = extFilter.order; + order = extFilter.order; + StringUtils::Replace(order, "album.", "sv."); + StringUtils::Replace(order, "song.", "sv."); + joinFilter.AppendOrder(order); + } + else + joinFilter.AppendOrder("sv.idSong"); + joinFilter.AppendGroup("sv.idSong"); + + // Album artists + if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) || + joinLayout.GetFetch(joinToSongs_strAlbumArtist) || + joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID)) + { // All songs have at least one album artist so inner join sufficient + bJoinAlbumArtist = true; + joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum"); + joinFilter.AppendGroup("album_artist.idArtist"); + joinFilter.AppendOrder("album_artist.iOrder"); + // Ensure idAlbumArtist is queried for processing repeats + if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist)) + { + joinLayout.SetField(joinToSongs_idAlbumArtist, + JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL); + } + // Ensure song.IdAlbum is field of the inline view for join + if (fields.find("albumid") == fields.end()) + { + extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField); + dbfieldindex.emplace_back(-1); + } + // artist table needed for strArtist or MBID + // (album_artist.strArtist can be an alias or spelling variation) + if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) || + joinLayout.GetFetch(joinToSongs_strAlbumArtist)) + joinFilter.AppendJoin( + "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist"); + } + + /* + Song artists + JSON schema "artist", "artistid", "musicbrainzartistid", "contributors", + "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist", + */ + if (joinLayout.GetFetch(joinToSongs_idArtist) || joinLayout.GetFetch(joinToSongs_strArtist) || + joinLayout.GetFetch(joinToSongs_strArtistMBID) || !rolefieldlist.empty()) + { // All songs have at least one artist (idRole = 1) so inner join sufficient + bJoinSongArtist = true; + if (rolefieldlist.empty()) + { // song artists only, no other roles needed + joinFilter.AppendJoin( + "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1"); + joinFilter.AppendGroup("song_artist.idArtist"); + joinFilter.AppendOrder("song_artist.iOrder"); + } + else + { + // Ensure idRole is queried + if (!joinLayout.GetFetch(joinToSongs_idRole)) + { + joinLayout.SetField(joinToSongs_idRole, + JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL); + } + // Ensure strArtist is queried + if (!joinLayout.GetFetch(joinToSongs_strArtist)) + { + joinLayout.SetField(joinToSongs_strArtist, + JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL); + } + if (fields.find("contributors") != fields.end()) + { // all roles + bJoinRole = true; + // Ensure strRole is queried from role table + joinLayout.SetField(joinToSongs_strRole, "role.strRole"); + joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong"); + joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole"); + joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole"); + joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist"); + } + else + { // Get just roles for "displaycomposer", "displayconductor" etc. + std::string where; + for (size_t i = 0; i < roleidlist.size(); i++) + { + int idRole = roleidlist[i]; + if (idRole <= 1) + continue; + if (where.empty()) + // Always get song artists too (role = 1) so can do inner join + where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole); + else + where += PrepareSQL(" OR song_artist.idRole = %i", idRole); + } + where = " (" + where + ")"; + joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where); + joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole"); + joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist"); + } + } + // Ensure idArtist is queried for processing repeats + if (!joinLayout.GetFetch(joinToSongs_idArtist)) + { + joinLayout.SetField(joinToSongs_idArtist, + JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL); + } + // artist table needed for strArtist or MBID + // (song_artist.strArtist can be an alias or spelling variation) + if (joinLayout.GetFetch(joinToSongs_strArtistMBID) || + joinLayout.GetFetch(joinToSongs_strArtist)) + joinFilter.AppendJoin( + "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist"); + } + + // Genre ids + if (joinLayout.GetFetch(joinToSongs_idGenre)) + { // song genre ids (strGenre demormalised in song table) + // Left join as songs may not have genre + joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong"); + joinFilter.AppendGroup("song_genre.idGenre"); + joinFilter.AppendOrder("song_genre.iOrder"); + } + + // Build JOIN part of query (if we have one) + std::string strSQLJoin; + if (joinLayout.HasFilterFields()) + if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin)) + return false; + + // Adjust where in the results record the join fields are allowing for the + // inline view fields (Quicker than finding field by name every time) + // idSong + other song fields + joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size())); + + // Build full query + // When have multiple value joins use inline view + // SELECT sv.*, <join fields> FROM + // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv + // <joins> <group by> + // <order by> + <joins order by> + // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator + strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra; + if (joinLayout.HasFilterFields()) + { + strSQL = "(" + strSQL + ") AS sv "; + strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin; + } + + // Modify query to use correct year field + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate"); + else + StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate"); + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + // Run query + auto start = std::chrono::steady_clock::now(); + + if (!m_pDS->query(strSQL)) + return false; + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count()); + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound <= 0) + { + m_pDS->close(); + return true; + } + + // Get song from returned rows. Joins mean there can be many rows per song + int songId = -1; + int albumartistId = -1; + int artistId = -1; + int roleId = -1; + bool bSongGenreDone(false); + bool bSongArtistDone(false); + bool bHaveSong(false); + CVariant songObj; + result["songs"].reserve(resultcount); + while (!m_pDS->eof() || bHaveSong) + { + const dbiplus::sql_record* const record = m_pDS->get_sql_record(); + + if (m_pDS->eof() || songId != record->at(0).get_asInt()) + { + // Store previous or last song + if (bHaveSong) + { + // Check empty role fields get returned, and format + if (!rolefieldlist.empty()) + { + for (const auto& displayXXX : rolefieldlist) + { + if (!StringUtils::StartsWith(displayXXX, "display")) + { + // "contributors" + if (!songObj.isMember(displayXXX)) + songObj[displayXXX] = CVariant(CVariant::VariantTypeArray); + } + else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray()) + { + // Convert "displaycomposer", "displayconductor", "displayorchestra", + // and "displaylyricist" arrays into strings + std::vector<std::string> names; + for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array(); + field != songObj[displayXXX].end_array(); ++field) + names.emplace_back(field->asString()); + + std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_musicItemSeparator); + songObj[displayXXX] = role; + } + else + songObj[displayXXX] = ""; + } + } + result["songs"].append(songObj); + bHaveSong = false; + songObj.clear(); + } + if (songObj.empty()) + { + // Initialise fields, ensure those with possible null values are set to correct empty variant type + if (joinLayout.GetOutput(joinToSongs_idGenre)) + songObj["genreid"] = + CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array + + albumartistId = -1; + artistId = -1; + roleId = -1; + bSongGenreDone = false; + bSongArtistDone = false; + } + if (m_pDS->eof()) + continue; // Having saved the last song stop + + // New song + songId = record->at(0).get_asInt(); + bHaveSong = true; + songObj["songid"] = songId; + songObj["label"] = record->at(1).get_asString(); + for (size_t i = 0; i < dbfieldindex.size(); i++) + if (dbfieldindex[i] > -1) + { + if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer") + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt(); + else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned") + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = + std::max(record->at(1 + i).get_asInt(), 0); + else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float") + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = + std::max(record->at(1 + i).get_asFloat(), 0.f); + else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array") + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split( + record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_musicItemSeparator); + else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean") + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool(); + else + songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString(); + } + + // Split sources string into int array + if (songObj.isMember("sourceid")) + { + std::vector<std::string> sources = + StringUtils::Split(songObj["sourceid"].asString(), ";"); + songObj["sourceid"] = CVariant(CVariant::VariantTypeArray); + for (size_t i = 0; i < sources.size(); i++) + songObj["sourceid"].append(atoi(sources[i].c_str())); + } + } + + if (bJoinAlbumArtist) + { + if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt()) + { + bSongGenreDone = + bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre + bSongArtistDone = + bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists + albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt(); + if (joinLayout.GetOutput(joinToSongs_idAlbumArtist)) + songObj["albumartistid"].append(albumartistId); + if (albumartistId == BLANKARTIST_ID) + { + if (joinLayout.GetOutput(joinToSongs_strAlbumArtist)) + songObj["albumartist"].append(StringUtils::Empty); + if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID)) + songObj["musicbrainzalbumartistid"].append(StringUtils::Empty); + } + else + { + if (joinLayout.GetOutput(joinToSongs_idAlbumArtist)) + songObj["albumartistid"].append(albumartistId); + if (joinLayout.GetOutput(joinToSongs_strAlbumArtist)) + songObj["albumartist"].append( + record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString()); + if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID)) + songObj["musicbrainzalbumartistid"].append( + record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString()); + } + } + } + if (bJoinSongArtist && !bSongArtistDone) + { + if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt()) + { + bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre + roleId = -1; // Allow for many artists same role + artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt(); + if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 || + record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1) + { + if (joinLayout.GetOutput(joinToSongs_idArtist)) + songObj["artistid"].append(artistId); + if (artistId == BLANKARTIST_ID) + { + if (joinLayout.GetOutput(joinToSongs_strArtist)) + songObj["artist"].append(StringUtils::Empty); + if (joinLayout.GetOutput(joinToSongs_strArtistMBID)) + songObj["musicbrainzartistid"].append(StringUtils::Empty); + } + else + { + if (joinLayout.GetOutput(joinToSongs_strArtist)) + songObj["artist"].append( + record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString()); + if (joinLayout.GetOutput(joinToSongs_strArtistMBID)) + songObj["musicbrainzartistid"].append( + record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString()); + } + } + } + if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 && + roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt()) + { + bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre + roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt(); + if (roleId > 1) + { + if (bJoinRole) + { //Contributors + CVariant contributor; + contributor["name"] = + record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString(); + contributor["role"] = + record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString(); + contributor["roleid"] = roleId; + contributor["artistid"] = + record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt(); + songObj["contributors"].append(contributor); + } + // "displaycomposer", "displayconductor" etc. + for (size_t i = 0; i < roleidlist.size(); i++) + { + if (roleidlist[i] == roleId) + { + songObj[rolefieldlist[i]].append( + record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString()); + continue; + } + } + } + } + } + if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 && + !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull()) + { + songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt()); + } + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + + // Ensure random order of output when results set is sorted to process multi-value joins + if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields()) + KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField) +{ + /* + Make SQL clause from ignore article list. + Group tokens the same length together, for example : + WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_' + THEN SUBSTR(strArtist, 5) + WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_' + THEN SUBSTR(strArtist, 4) + */ + std::set<std::string> sortTokens = g_langInfo.GetSortTokens(); + std::string sortclause; + size_t tokenlength = 0; + std::string strWhen; + for (const auto& token : sortTokens) + { + if (token.length() != tokenlength) + { + if (!strWhen.empty()) + { + if (!sortclause.empty()) + sortclause += " "; + std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1); + sortclause += "WHEN " + strWhen + strThen; + strWhen.clear(); + } + tokenlength = token.length(); + } + std::string tokenclause = token; + //Escape any ' or % in the token + StringUtils::Replace(tokenclause, "'", "''"); + StringUtils::Replace(tokenclause, "%", "%%"); + // Single %, _ and ' so avoid using PrepareSQL + tokenclause = strField + " LIKE '" + tokenclause + "%'"; + if (token.find('_') != std::string::npos) + tokenclause += " ESCAPE '_'"; + if (!strWhen.empty()) + strWhen += " OR "; + strWhen += tokenclause; + } + if (!strWhen.empty()) + { + if (!sortclause.empty()) + sortclause += " "; + std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1); + sortclause += "WHEN " + strWhen + strThen; + } + return sortclause; +} + +std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias, + const SortAttribute& sortAttributes, + const std::string& strField, + const std::string& strSortField) +{ + /* + Build SQL for sort name scalar subquery from sort attributes and ignore article list. + For example : + CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort + WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5) + WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4) + ELSE strField + END AS strAlias + */ + + std::string sortSQL; + if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName) + sortSQL = + PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str()); + if (sortAttributes & SortAttributeIgnoreArticle) + { + if (!sortSQL.empty()) + sortSQL += " "; + // Make SQL from ignore article list, grouping tokens the same length together + sortSQL += GetIgnoreArticleSQL(strField); + } + if (!sortSQL.empty()) + { + sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc. + sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str()); + } + + return sortSQL; +} + +std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField, + const SortOrder& sortOrder) +{ + /* + Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity + and locale UFT-8 order for accents using the same functionality as fileitem list sorting. + Natural number order is not significant for where clause comparison and use of calculated fields + means there is no advantage in defining as column default in table create than per query (which + also makes looking at the db with other tools difficult). + + MySQL does not have callback collation, but all tables are defined with utf8_general_ci an + "ascii folding" case insensitive collation. Natural sorting is provided via native functions + stored in the db. + */ + std::string DESC; + if (sortOrder == SortOrderDescending) + DESC = " DESC"; + std::string strSort; + + if (StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, + "mysql")) + strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str()); + else + strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str()); + return strSort; +} + +void CMusicDatabase::UpdateTables(int version) +{ + CLog::Log(LOGINFO, "{} - updating tables", __FUNCTION__); + if (version < 34) + { + m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n"); + m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n"); + m_pDS->exec( + "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, " + "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration " + "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, " + "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, " + "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n"); + m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, " + "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, " + "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) " + "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, " + "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, " + "iEndOffset, idThumb, lastplayed, rating, comment FROM song"); + + m_pDS->exec("DROP TABLE song"); + m_pDS->exec("ALTER TABLE song_new RENAME TO song"); + + m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL"); + } + + if (version < 36) + { + // translate legacy musicdb:// paths + if (m_pDS->query("SELECT strPath FROM content")) + { + std::vector<std::string> contentPaths; + while (!m_pDS->eof()) + { + contentPaths.push_back(m_pDS->fv(0).get_asString()); + m_pDS->next(); + } + m_pDS->close(); + + for (const auto& originalPath : contentPaths) + { + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath); + m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(), + originalPath.c_str())); + } + } + } + + if (version < 39) + { + m_pDS->exec("CREATE TABLE album_new " + "(idAlbum integer primary key, " + " strAlbum varchar(256), strMusicBrainzAlbumID text, " + " strArtists text, strGenres text, " + " iYear integer, idThumb integer, " + " bCompilation integer not null default '0', " + " strMoods text, strStyles text, strThemes text, " + " strReview text, strImage text, strLabel text, " + " strType text, " + " iRating integer, " + " lastScraped varchar(20) default NULL, " + " dateAdded varchar (20) default NULL)"); + m_pDS->exec("INSERT INTO album_new " + "(idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, " + " iRating) " + " SELECT " + " album.idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " album.iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, iRating " + " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum"); + m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE " + "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)"); + m_pDS->exec(PrepareSQL( + "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)", + CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str())); + m_pDS->exec("DROP TABLE album"); + m_pDS->exec("DROP TABLE albuminfo"); + m_pDS->exec("ALTER TABLE album_new RENAME TO album"); + } + if (version < 40) + { + m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, " + " strArtist varchar(256), strMusicBrainzArtistID text, " + " strBorn text, strFormed text, strGenres text, strMoods text, " + " strStyles text, strInstruments text, strBiography text, " + " strDied text, strDisbanded text, strYearsActive text, " + " strImage text, strFanart text, " + " lastScraped varchar(20) default NULL, " + " dateAdded varchar (20) default NULL)"); + m_pDS->exec("INSERT INTO artist_new " + "(idArtist, strArtist, strMusicBrainzArtistID, " + " strBorn, strFormed, strGenres, strMoods, " + " strStyles , strInstruments , strBiography , " + " strDied, strDisbanded, strYearsActive, " + " strImage, strFanart) " + " SELECT " + " artist.idArtist, " + " strArtist, strMusicBrainzArtistID, " + " strBorn, strFormed, strGenres, strMoods, " + " strStyles, strInstruments, strBiography, " + " strDied, strDisbanded, strYearsActive, " + " strImage, strFanart " + " FROM artist " + " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist"); + m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT " + "idArtist FROM artistinfo)", + CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str())); + m_pDS->exec("DROP TABLE artist"); + m_pDS->exec("DROP TABLE artistinfo"); + m_pDS->exec("ALTER TABLE artist_new RENAME TO artist"); + } + if (version < 42) + { + m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n"); + m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n"); + // populate these + std::string sql = "select idArtist,strArtist from artist"; + m_pDS->query(sql); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i", + m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt())); + m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i", + m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + } + if (version < 48) + { // null out columns that are no longer used + m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL"); + m_pDS->exec("UPDATE album SET idThumb=NULL"); + } + if (version < 49) + { + m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)"); + } + if (version < 50) + { + // add a new column strReleaseType for albums + m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n"); + + // set strReleaseType based on album name + m_pDS->exec(PrepareSQL( + "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''", + CAlbum::ReleaseTypeToString(CAlbum::Album).c_str())); + m_pDS->exec( + PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''", + CAlbum::ReleaseTypeToString(CAlbum::Single).c_str())); + } + if (version < 51) + { + m_pDS->exec("ALTER TABLE song ADD mood text\n"); + } + if (version < 53) + { + m_pDS->exec("ALTER TABLE song ADD dateAdded text"); + } + if (version < 54) + { + //Remove dateAdded from artist table + m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, " + " strArtist varchar(256), strMusicBrainzArtistID text, " + " strBorn text, strFormed text, strGenres text, strMoods text, " + " strStyles text, strInstruments text, strBiography text, " + " strDied text, strDisbanded text, strYearsActive text, " + " strImage text, strFanart text, " + " lastScraped varchar(20) default NULL)"); + m_pDS->exec("INSERT INTO artist_new " + "(idArtist, strArtist, strMusicBrainzArtistID, " + " strBorn, strFormed, strGenres, strMoods, " + " strStyles , strInstruments , strBiography , " + " strDied, strDisbanded, strYearsActive, " + " strImage, strFanart, lastScraped) " + " SELECT " + " idArtist, " + " strArtist, strMusicBrainzArtistID, " + " strBorn, strFormed, strGenres, strMoods, " + " strStyles, strInstruments, strBiography, " + " strDied, strDisbanded, strYearsActive, " + " strImage, strFanart, lastScraped " + " FROM artist"); + m_pDS->exec("DROP TABLE artist"); + m_pDS->exec("ALTER TABLE artist_new RENAME TO artist"); + + //Remove dateAdded from album table + m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, " + " strAlbum varchar(256), strMusicBrainzAlbumID text, " + " strArtists text, strGenres text, " + " iYear integer, idThumb integer, " + " bCompilation integer not null default '0', " + " strMoods text, strStyles text, strThemes text, " + " strReview text, strImage text, strLabel text, " + " strType text, " + " iRating integer, " + " lastScraped varchar(20) default NULL, " + " strReleaseType text)"); + m_pDS->exec("INSERT INTO album_new " + "(idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, iRating, lastScraped, " + " strReleaseType) " + " SELECT " + " album.idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, iRating, lastScraped, " + " strReleaseType" + " FROM album"); + m_pDS->exec("DROP TABLE album"); + m_pDS->exec("ALTER TABLE album_new RENAME TO album"); + } + if (version < 55) + { + m_pDS->exec("DROP TABLE karaokedata"); + } + if (version < 57) + { + m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0"); + m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL"); + m_pDS->exec("UPDATE song SET userrating = rating * 2"); + m_pDS->exec("UPDATE song SET rating = 0"); + m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, " + " idAlbum INTEGER, idPath INTEGER, " + " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), " + " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, " + " dwFileNameCRC TEXT, " + " strFileName TEXT, strMusicBrainzTrackID TEXT, " + " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, " + " idThumb INTEGER, " + " lastplayed VARCHAR(20) DEFAULT NULL, " + " rating FLOAT DEFAULT 0, " + " userrating INTEGER DEFAULT 0, " + " comment TEXT, mood TEXT, dateAdded TEXT)"); + m_pDS->exec("INSERT INTO song_new " + "(idSong, " + " idAlbum, idPath, " + " strArtists, strGenres, strTitle, " + " iTrack, iDuration, iYear, " + " dwFileNameCRC, " + " strFileName, strMusicBrainzTrackID, " + " iTimesPlayed, iStartOffset, iEndOffset, " + " idThumb, " + " lastplayed," + " rating, userrating, " + " comment, mood, dateAdded)" + " SELECT " + " idSong, " + " idAlbum, idPath, " + " strArtists, strGenres, strTitle, " + " iTrack, iDuration, iYear, " + " dwFileNameCRC, " + " strFileName, strMusicBrainzTrackID, " + " iTimesPlayed, iStartOffset, iEndOffset, " + " idThumb, " + " lastplayed," + " rating, " + " userrating, " + " comment, mood, dateAdded" + " FROM song"); + m_pDS->exec("DROP TABLE song"); + m_pDS->exec("ALTER TABLE song_new RENAME TO song"); + + m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0"); + m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL"); + m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, " + " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, " + " strArtists TEXT, strGenres TEXT, " + " iYear INTEGER, idThumb INTEGER, " + " bCompilation INTEGER NOT NULL DEFAULT '0', " + " strMoods TEXT, strStyles TEXT, strThemes TEXT, " + " strReview TEXT, strImage TEXT, strLabel TEXT, " + " strType TEXT, " + " fRating FLOAT NOT NULL DEFAULT 0, " + " iUserrating INTEGER NOT NULL DEFAULT 0, " + " lastScraped VARCHAR(20) DEFAULT NULL, " + " strReleaseType TEXT)"); + m_pDS->exec("INSERT INTO album_new " + "(idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, " + " fRating, " + " iUserrating, " + " lastScraped, " + " strReleaseType)" + " SELECT " + " idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, strGenres, " + " iYear, idThumb, " + " bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, " + " iRating, " + " iUserrating, " + " lastScraped, " + " strReleaseType" + " FROM album"); + m_pDS->exec("DROP TABLE album"); + m_pDS->exec("ALTER TABLE album_new RENAME TO album"); + + m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0"); + m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0"); + } + if (version < 58) + { + m_pDS->exec("UPDATE album SET fRating = fRating * 2"); + } + if (version < 59) + { + m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)"); + m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role + + //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole + m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, " + "iOrder integer, strArtist text)"); + m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) " + "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist"); + m_pDS->exec("DROP TABLE song_artist"); + m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist"); + + //Remove strJoinPhrase, boolFeatured from album_artist table + m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, " + "strArtist text)"); + m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) " + "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist"); + m_pDS->exec("DROP TABLE album_artist"); + m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist"); + } + if (version < 60) + { + // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that + // do not have an artist tag to ensure all songs in the library have at least one artist. + std::string strSQL; + if (GetArtistExists(BLANKARTIST_ID)) + { + // When BLANKARTIST_ID (=1) is already in use, move the record + try + { //No mbid index yet, so can have record for artist twice even with mbid + strSQL = PrepareSQL("INSERT INTO artist SELECT null, " + "strArtist, strMusicBrainzArtistID, " + "strBorn, strFormed, strGenres, strMoods, " + "strStyles, strInstruments, strBiography, " + "strDied, strDisbanded, strYearsActive, " + "strImage, strFanart, lastScraped " + "FROM artist WHERE artist.idArtist = %i", + BLANKARTIST_ID); + m_pDS->exec(strSQL); + int idArtist = (int)m_pDS->lastinsertid(); + //No triggers, so can delete artist without effecting other tables. + strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID); + m_pDS->exec(strSQL); + + // Update related tables with the new artist ID + // Indices have been dropped making transactions very slow, so create appropriate temp indices + m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )"); + m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )"); + m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )"); + m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )"); + strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist, + BLANKARTIST_ID); + m_pDS->exec(strSQL); + strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist, + BLANKARTIST_ID); + m_pDS->exec(strSQL); + strSQL = + PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'", + idArtist, BLANKARTIST_ID); + m_pDS->exec(strSQL); + strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist, + BLANKARTIST_ID); + m_pDS->exec(strSQL); + // Drop temp indices + m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist"); + m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist"); + m_pDS->exec("DROP INDEX idxDiscography ON discography"); + m_pDS->exec("DROP INDEX ix_art ON art"); + } + catch (...) + { + CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed"); + } + } + + // Create missing artist tag artist [Missing]. + // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs + strSQL = PrepareSQL( + "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )", + BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str()); + m_pDS->exec(strSQL); + + // Indices have been dropped making transactions very slow, so create temp index + m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )"); + m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )"); + + // Ensure all songs have at least one artist, set those without to [Missing] + strSQL = "SELECT count(idSong) FROM song " + "WHERE NOT EXISTS(SELECT idSong FROM song_artist " + "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)"; + int numsongs = GetSingleValueInt(strSQL); + if (numsongs > 0) + { + CLog::Log(LOGDEBUG, "{} songs have no artist, setting artist to [Missing]", numsongs); + // Insert song_artist records for songs that don't have any + try + { + strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) " + "SELECT %i, idSong, %i, '%s', 0 FROM song " + "WHERE NOT EXISTS(SELECT idSong FROM song_artist " + "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)", + BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST); + ExecuteQuery(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed"); + } + } + + // Ensure all albums have at least one artist, set those without to [Missing] + strSQL = "SELECT count(idAlbum) FROM album " + "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist " + "WHERE album_artist.idAlbum = album.idAlbum)"; + int numalbums = GetSingleValueInt(strSQL); + if (numalbums > 0) + { + CLog::Log(LOGDEBUG, "{} albums have no artist, setting artist to [Missing]", numalbums); + // Insert album_artist records for albums that don't have any + try + { + strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) " + "SELECT %i, idAlbum, '%s', 0 FROM album " + "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist " + "WHERE album_artist.idAlbum = album.idAlbum)", + BLANKARTIST_ID, BLANKARTIST_NAME.c_str()); + ExecuteQuery(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed"); + } + } + //Remove temp indices, full analytics for database created later + m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist"); + m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist"); + } + if (version < 61) + { + // Create versiontagscan table + m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)"); + m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)"); + } + if (version < 62) + { + CLog::Log(LOGINFO, "create audiobook table"); + m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, " + " strBook varchar(256), strAuthor text," + " bookmark integer, file text," + " dateAdded varchar (20) default NULL)"); + } + if (version < 63) + { + // Add strSortName to Artist table + m_pDS->exec("ALTER TABLE artist ADD strSortName text\n"); + + //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table + m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, " + " strAlbum varchar(256), strMusicBrainzAlbumID text, " + " strArtistDisp text, strArtistSort text, strGenres text, " + " iYear integer, bCompilation integer not null default '0', " + " strMoods text, strStyles text, strThemes text, " + " strReview text, strImage text, strLabel text, " + " strType text, " + " fRating FLOAT NOT NULL DEFAULT 0, " + " iUserrating INTEGER NOT NULL DEFAULT 0, " + " lastScraped varchar(20) default NULL, " + " strReleaseType text, " + " iVotes INTEGER NOT NULL DEFAULT 0)"); + m_pDS->exec("INSERT INTO album_new " + "(idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtistDisp, strArtistSort, strGenres, " + " iYear, bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, " + " fRating, iUserrating, iVotes, " + " lastScraped, " + " strReleaseType)" + " SELECT " + " idAlbum, " + " strAlbum, strMusicBrainzAlbumID, " + " strArtists, NULL, strGenres, " + " iYear, bCompilation, " + " strMoods, strStyles, strThemes, " + " strReview, strImage, strLabel, " + " strType, " + " fRating, iUserrating, iVotes, " + " lastScraped, " + " strReleaseType" + " FROM album"); + m_pDS->exec("DROP TABLE album"); + m_pDS->exec("ALTER TABLE album_new RENAME TO album"); + + //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table + m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, " + " idAlbum INTEGER, idPath INTEGER, " + " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), " + " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, " + " strFileName TEXT, strMusicBrainzTrackID TEXT, " + " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, " + " lastplayed VARCHAR(20) DEFAULT NULL, " + " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, " + " userrating INTEGER NOT NULL DEFAULT 0, " + " comment TEXT, mood TEXT, dateAdded TEXT)"); + m_pDS->exec("INSERT INTO song_new " + "(idSong, " + " idAlbum, idPath, " + " strArtistDisp, strArtistSort, strGenres, strTitle, " + " iTrack, iDuration, iYear, " + " strFileName, strMusicBrainzTrackID, " + " iTimesPlayed, iStartOffset, iEndOffset, " + " lastplayed," + " rating, userrating, votes, " + " comment, mood, dateAdded)" + " SELECT " + " idSong, " + " idAlbum, idPath, " + " strArtists, NULL, strGenres, strTitle, " + " iTrack, iDuration, iYear, " + " strFileName, strMusicBrainzTrackID, " + " iTimesPlayed, iStartOffset, iEndOffset, " + " lastplayed," + " rating, userrating, votes, " + " comment, mood, dateAdded" + " FROM song"); + m_pDS->exec("DROP TABLE song"); + m_pDS->exec("ALTER TABLE song_new RENAME TO song"); + } + if (version < 65) + { + // Remove cue table + m_pDS->exec("DROP TABLE cue"); + // Add strReplayGain to song table + m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n"); + } + if (version < 66) + { + // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums + m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n"); + m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n"); + // Add a new column bScrapedMBID for artists + m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n"); + } + if (version < 67) + { + // Add infosetting table + m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, " + "strSettings TEXT)"); + // Add a new column for setting to album and artist tables + m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n"); + m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n"); + + // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path + m_pDS->exec( + "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, " + "strScraperPath text, strSettings text)"); + try + { + m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) " + "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, " + "strScraperPath, strSettings " + "FROM content WHERE strContent = 'artists' AND strPath LIKE " + "'musicdb://artists/_%/' ORDER BY idItem"); + } + catch (...) + { + CLog::Log(LOGERROR, + "Migrating specific artist scraper settings has failed, settings not transferred"); + } + try + { + m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) " + "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, " + "strScraperPath, strSettings " + "FROM content WHERE strContent = 'albums' AND strPath LIKE " + "'musicdb://albums/_%/' ORDER BY idItem"); + } + catch (...) + { + CLog::Log(LOGERROR, + "Migrating specific album scraper settings has failed, settings not transferred"); + } + try + { + m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) " + "SELECT id, strScraperPath, strSettings FROM content_temp"); + m_pDS->exec( + "UPDATE artist SET idInfoSetting = " + "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) " + "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = " + "idArtist) "); + m_pDS->exec("UPDATE album SET idInfoSetting = " + "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) " + "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem " + "= idAlbum) "); + } + catch (...) + { + CLog::Log(LOGERROR, + "Migrating album and artist scraper settings has failed, settings not transferred"); + } + m_pDS->exec("DROP TABLE content_temp"); + + // Remove content table + m_pDS->exec("DROP TABLE content"); + // Remove albuminfosong table + m_pDS->exec("DROP TABLE albuminfosong"); + } + if (version < 68) + { + // Add a new columns strType, strGender, strDisambiguation for artists + m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n"); + m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n"); + m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n"); + } + if (version < 69) + { + // Remove album_genre table + m_pDS->exec("DROP TABLE album_genre"); + } + if (version < 70) + { + // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75) + m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * " + "40 / 3 \n"); + } + if (version < 71) + { + // Add lastscanned to versiontagscan table + m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n"); + CDateTime dateAdded = CDateTime::GetCurrentDateTime(); + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'", + dateAdded.GetAsDBDateTime().c_str())); + } + if (version < 72) + { + // Create source table + m_pDS->exec( + "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)"); + // Create source_path table + m_pDS->exec( + "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))"); + // Create album_source table + m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)"); + // Populate source and source_path tables from sources.xml + // Filling album_source needs to be done after indexes are created or it is + // very slow. It could be populated during CreateAnalytics but it is checked + // and filled as part of scanning anyway so simply force full rescan. + MigrateSources(); + } + if (version < 73) + { + // add bBoxedSet to album table + m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n"); + // add iDiscTotal to album table + m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n"); + // populate iDiscTotal from the data already in the song table + m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) " + "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) " + "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)"); + // add strDiscSubtitles to song table + m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n"); + } + if (version < 74) + { + //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table + m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, " + "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, " + "strReleaseGroupMBID TEXT, " + "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, " + "strReleaseDate TEXT, strOrigReleaseDate TEXT, " + "bBoxedSet INTEGER NOT NULL DEFAULT 0, " + "bCompilation INTEGER NOT NULL DEFAULT '0', " + "strMoods TEXT, strStyles TEXT, strThemes TEXT, " + "strReview TEXT, strImage TEXT, strLabel TEXT, " + "strType TEXT, " + "fRating FLOAT NOT NULL DEFAULT 0, " + "iVotes INTEGER NOT NULL DEFAULT 0, " + "iUserrating INTEGER NOT NULL DEFAULT 0, " + "lastScraped VARCHAR(20) DEFAULT NULL, " + "bScrapedMBID INTEGER NOT NULL DEFAULT 0, " + "strReleaseType TEXT, " + "iDiscTotal INTEGER NOT NULL DEFAULT 0, " + "idInfoSetting INTEGER NOT NULL DEFAULT 0)"); + // Prepare as MySQL has different CAST datatypes + m_pDS->exec( + PrepareSQL("INSERT INTO album_new " + "(idalbum, strAlbum, " + "strMusicBrainzAlbumID, strReleaseGroupMBID, " + "strArtistDisp, strArtistSort, strGenres, " + "strReleaseDate, strOrigReleaseDate, " + "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, " + "strReview, strImage, strLabel, strType, " + "fRating, iVotes, iUserrating, " + "lastScraped, bScrapedMBID, strReleaseType, " + "iDiscTotal, idInfoSetting) " + "SELECT " + "idAlbum, strAlbum, " + "strMusicBrainzAlbumID, strReleaseGroupMBID, " + "strArtistDisp, strArtistSort, strGenres, " + "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, " + "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, " + // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now + "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, " + "bCompilation, strMoods, strStyles, strThemes, " + "strReview, strImage, strLabel, strType, " + "fRating, iVotes, iUserrating, " + "lastScraped, bScrapedMBID, strReleaseType, " + "iDiscTotal, idInfoSetting " + "FROM album")); + m_pDS->exec("DROP TABLE album"); + m_pDS->exec("ALTER TABLE album_new RENAME TO album"); + + //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table + m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, " + "idAlbum INTEGER, idPath INTEGER, " + "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), " + "iTrack INTEGER, iDuration INTEGER, " + "strReleaseDate TEXT, strOrigReleaseDate TEXT, " + "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, " + "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, " + "lastplayed VARCHAR(20) DEFAULT NULL, " + "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, " + "userrating INTEGER NOT NULL DEFAULT 0, " + "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, " + "dateAdded TEXT)"); + // Prepare as MySQL has different CAST datatypes + m_pDS->exec(PrepareSQL("INSERT INTO song_new " + "(idSong, " + "idAlbum, idPath, " + "strArtistDisp, strArtistSort, strGenres, strTitle, " + "iTrack, iDuration, " + "strReleaseDate, strOrigReleaseDate, " + "strDiscSubtitle, strFileName, strMusicBrainzTrackID, " + "iTimesPlayed, iStartOffset, iEndOffset, " + "lastplayed, " + "rating, userrating, votes, " + "comment, mood, strReplayGain, dateAdded) " + "SELECT " + "idSong, " + "idAlbum, idPath, " + "strArtistDisp, strArtistSort, strGenres, strTitle, " + "iTrack, iDuration, " + "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, " + "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, " + "strDiscSubtitle, strFileName, strMusicBrainzTrackID, " + "iTimesPlayed, iStartOffset, iEndOffset, " + "lastplayed, " + "rating, userrating, votes, " + "comment, mood, strReplayGain, dateAdded " + "FROM song")); + m_pDS->exec("DROP TABLE song"); + m_pDS->exec("ALTER TABLE song_new RENAME TO song"); + } + if (version < 75) + { + m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0"); + m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0"); + m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0"); + } + if (version < 77) + { + m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT"); + } + if (version < 78) + { + std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime(); + + // Add removed_link table + m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)"); + // Add lastcleaned and artistlinksupdated to versiontagscan table + m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)"); + m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)"); + m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)"); + // Adjust lastscanned if original local time value is after current UTC + if (GetLibraryLastUpdated() > strUTCNow) + SetLibraryLastUpdated(); + m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, " + "genresupdated = lastscanned, " + "artistlinksupdated = lastscanned"); + + // Add dateNew, dateModified to song table + m_pDS->exec("ALTER TABLE song ADD dateNew TEXT"); + m_pDS->exec("ALTER TABLE song ADD dateModified TEXT"); + // Set new to dateAdded and modified to lastplayed as estimates + // Limit those local time values to now UTC, and modified is after new + m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed"); + m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow.c_str(), + strUTCNow.c_str())); + m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL"); + m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'", + strUTCNow.c_str(), strUTCNow.c_str())); + m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified"); + + // Add dateAdded, dateNew, dateModified to album table + m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT"); + m_pDS->exec("ALTER TABLE album ADD dateNew TEXT"); + m_pDS->exec("ALTER TABLE album ADD dateModified TEXT"); + // Set dateAdded and new values from song dates, and modified to lastscraped as estimates + // Limit modified value to now UTC and after new + // Indices have been dropped making subquery very slow, so create temp index + m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)"); + m_pDS->exec("UPDATE album SET dateAdded = " + "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)"); + m_pDS->exec("UPDATE album SET dateNew = " + "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)"); + m_pDS->exec("UPDATE album SET dateModified = dateNew"); + m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified"); + m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'", + strUTCNow.c_str(), strUTCNow.c_str())); + //Remove temp index, full analytics for database created later + m_pDS->exec("DROP INDEX idxSong3 ON song"); + + // Add dateAdded, dateNew, dateModified to artist table + m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT"); + m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT"); + m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT"); + // dateAdded has NULL values until files rescanned by user + // Set new and modified to now UTC as not worth complexity of estimating from song dates + m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str())); + m_pDS->exec("UPDATE artist SET dateModified = dateNew"); + } + if (version < 79) + { + m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT"); + } + if (version < 80) + { + m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0"); + // update duration for all current albums + m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song " + "WHERE song.idAlbum = album.idAlbum) " + "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)"); + } + if (version < 82) + { + // Update artist table combining fanart URL data into strImage field + // Clear empty URL data <fanart /> and <thumb /> + m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'"); + m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'"); + //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs + m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')"); + m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')"); + m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb " + "aspect=\"fanart\" preview')"); + // Art URLs limited on MySQL databases to 65535 characters (TEXT field) + // Truncate the fanart when total URLs exceeds this + bool bisMySQL = StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, + "mysql"); + if (bisMySQL) + { + std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist " + "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535"; + if (m_pDS->query(strSQL)) + { + while (!m_pDS->eof()) + { + int idArtist = m_pDS->fv("idArtist").get_asInt(); + std::string strFanart = m_pDS->fv("strFanart").get_asString(); + std::string strImage = m_pDS->fv("strImage").get_asString(); + size_t space = 65535; + // Trim strImage to allow arbitrary half space for fanart + if (!TrimImageURLs(strImage, space / 2)) + strImage.clear(); // </thumb> not found, empty field + space = space - strImage.length(); + // Trim fanart to fit remaining space + if (!TrimImageURLs(strFanart, space)) + strFanart.clear(); // </thumb> not found, empty field + + strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' " + "WHERE idArtist = %i", + strFanart.c_str(), strImage.c_str(), idArtist); + m_pDS2->exec(strSQL); // Use other dataset to update while looping result set + + m_pDS->next(); + } + m_pDS->close(); + } + } + + // Remove strFanart column from artist table + m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, " + "strArtist varchar(256), strMusicBrainzArtistID text, " + "strSortName text, " + "strType text, strGender text, strDisambiguation text, " + "strBorn text, strFormed text, strGenres text, strMoods text, " + "strStyles text, strInstruments text, strBiography text, " + "strDied text, strDisbanded text, strYearsActive text, " + "strImage text, " + "lastScraped varchar(20) default NULL, " + "bScrapedMBID INTEGER NOT NULL DEFAULT 0, " + "idInfoSetting INTEGER NOT NULL DEFAULT 0, " + "dateAdded TEXT, dateNew TEXT, dateModified TEXT)"); + // Concatenate fanart URLs into strImage field + // Prepare SQL to convert CONCAT to || in SQLite + m_pDS->exec(PrepareSQL("INSERT INTO artist_new " + "(idArtist, strArtist, strMusicBrainzArtistID, " + "strSortName, strType, strGender, strDisambiguation, " + "strBorn, strFormed, strGenres, strMoods, " + "strStyles , strInstruments , strBiography , " + "strDied, strDisbanded, strYearsActive, " + "strImage, " + "lastScraped, bScrapedMBID, idInfoSetting, " + "dateAdded, dateNew, dateModified) " + "SELECT " + "artist.idArtist, " + "strArtist, strMusicBrainzArtistID, " + "strSortName, strType, strGender, strDisambiguation, " + "strBorn, strFormed, strGenres, strMoods, " + "strStyles, strInstruments, strBiography, " + "strDied, strDisbanded, strYearsActive, " + "CONCAT(strImage, strFanart), " + "lastScraped, bScrapedMBID, idInfoSetting, " + "dateAdded, dateNew, dateModified " + "FROM artist")); + m_pDS->exec("DROP TABLE artist"); + m_pDS->exec("ALTER TABLE artist_new RENAME TO artist"); + } + // Set the version of tag scanning required. + // Not every schema change requires the tags to be rescanned, set to the highest schema version + // that needs this. Forced rescanning (of music files that have not changed since they were + // previously scanned) also accommodates any changes to the way tags are processed + // e.g. read tags that were not processed by previous versions. + // The original db version when the tags were scanned, and the minimal db version needed are + // later used to determine if a forced rescan should be prompted + + // The last schema change needing forced rescanning was 73. + // This is because Kodi can now read and process extra tags involved in the creation of box sets + + SetMusicNeedsTagScan(73); + + // After all updates, store the original db version. + // This indicates the version of tag processing that was used to populate db + SetMusicTagScanVersion(version); +} + +int CMusicDatabase::GetSchemaVersion() const +{ + return 82; +} + +int CMusicDatabase::GetMusicNeedsTagScan() +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string sql = "SELECT * FROM versiontagscan"; + if (!m_pDS->query(sql)) + return -1; + + if (m_pDS->num_rows() != 1) + { + m_pDS->close(); + return -1; + } + + int idVersion = m_pDS->fv("idVersion").get_asInt(); + int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt(); + m_pDS->close(); + if (idVersion < iNeedsScan) + return idVersion; + else + return 0; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +void CMusicDatabase::SetMusicNeedsTagScan(int version) +{ + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version)); +} + +void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */) +{ + if (version == 0) + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion())); + else + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version)); +} + +std::string CMusicDatabase::GetLibraryLastUpdated() +{ + return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1"); +} + +void CMusicDatabase::SetLibraryLastUpdated() +{ + CDateTime dateUpdated = CDateTime::GetUTCDateTime(); + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'", + dateUpdated.GetAsDBDateTime().c_str())); +} + +std::string CMusicDatabase::GetLibraryLastCleaned() +{ + return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1"); +} + +void CMusicDatabase::SetLibraryLastCleaned() +{ + std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime(); + m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated.c_str())); +} + +std::string CMusicDatabase::GetArtistLinksUpdated() +{ + return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1"); +} + +void CMusicDatabase::SetArtistLinksUpdated() +{ + std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime(); + m_pDS->exec( + PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated.c_str())); +} + +std::string CMusicDatabase::GetGenresLastAdded() +{ + return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1"); +} + +std::string CMusicDatabase::GetSongsLastAdded() +{ + return GetSingleValue("SELECT MAX(dateNew) FROM song"); +} + +std::string CMusicDatabase::GetAlbumsLastAdded() +{ + return GetSingleValue("SELECT MAX(dateNew) FROM album"); +} + +std::string CMusicDatabase::GetArtistsLastAdded() +{ + return GetSingleValue("SELECT MAX(dateNew) FROM artist"); +} + +std::string CMusicDatabase::GetSongsLastModified() +{ + return GetSingleValue("SELECT MAX(dateModified) FROM song"); +} + +std::string CMusicDatabase::GetAlbumsLastModified() +{ + return GetSingleValue("SELECT MAX(dateModified) FROM album"); +} + +std::string CMusicDatabase::GetArtistsLastModified() +{ + return GetSingleValue("SELECT MAX(dateModified) FROM artist"); +} + +unsigned int CMusicDatabase::GetRandomSongIDs(const Filter& filter, + std::vector<std::pair<int, int>>& songIDs) +{ + try + { + if (nullptr == m_pDB) + return 0; + if (nullptr == m_pDS) + return 0; + + std::string strSQL = "SELECT idSong FROM songview "; + if (!CDatabase::BuildSQL(strSQL, filter, strSQL)) + return false; + strSQL += PrepareSQL(" ORDER BY RANDOM()"); + + if (!m_pDS->query(strSQL)) + return 0; + songIDs.clear(); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return 0; + } + songIDs.reserve(m_pDS->num_rows()); + while (!m_pDS->eof()) + { + songIDs.push_back(std::make_pair<int, int>(1, m_pDS->fv(song_idSong).get_asInt())); + m_pDS->next(); + } // cleanup + m_pDS->close(); + return static_cast<unsigned int>(songIDs.size()); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where); + } + return 0; +} + +int CMusicDatabase::GetSongsCount(const Filter& filter) +{ + try + { + if (nullptr == m_pDB) + return 0; + if (nullptr == m_pDS) + return 0; + + std::string strSQL = "select count(idSong) as NumSongs from songview "; + if (!CDatabase::BuildSQL(strSQL, filter, strSQL)) + return false; + + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return 0; + } + + int iNumSongs = m_pDS->fv("NumSongs").get_asInt(); + // cleanup + m_pDS->close(); + return iNumSongs; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where); + } + return 0; +} + +bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath) +{ + basePath.clear(); + std::vector<std::pair<std::string, int>> paths; + if (!GetAlbumPaths(idAlbum, paths)) + return false; + + for (const auto& pathpair : paths) + { + if (basePath.empty()) + basePath = pathpair.first.c_str(); + else + URIUtils::GetCommonPath(basePath, pathpair.first.c_str()); + } + return true; +} + +bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths) +{ + paths.clear(); + std::string strSQL; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; + + // Get the unique paths of songs on the album, providing there are no songs from + // other albums with the same path. This returns + // a) <album> if is contains all the songs and no others, or + // b) <album>/cd1, <album>/cd2 etc. for disc sets + // but does *not* return any path when albums are mixed together. That could be because of + // deliberate file organisation, or (more likely) because of a tagging error in album name + // or Musicbrainzalbumid. Thus it avoids finding some generic music path. + strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song " + "JOIN path ON song.idPath = path.idPath " + "WHERE song.idAlbum = %ld " + "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 " + "WHERE idPath = song.idPath) = 1", + idAlbum); + + if (!m_pDS2->query(strSQL)) + return false; + if (m_pDS2->num_rows() == 0) + { + // Album does not have a unique path, files are mixed + m_pDS2->close(); + return false; + } + + while (!m_pDS2->eof()) + { + paths.emplace_back(m_pDS2->fv("strPath").get_asString(), + m_pDS2->fv("song.idPath").get_asInt()); + m_pDS2->next(); + } + // Cleanup recordset data + m_pDS2->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL); + } + + return false; +} + +int CMusicDatabase::GetDiscnumberForPathID(int idPath) +{ + std::string strSQL; + int result = -1; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS2) + return -1; + + strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song " + "WHERE idPath = %i", + idPath); + + if (!m_pDS2->query(strSQL)) + return -1; + if (m_pDS2->num_rows() == 1) + { // Songs with this path have a unique disc number + result = m_pDS2->fv("discnum").get_asInt(); + } + // Cleanup recordset data + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL); + } + return result; +} + +// Get old "artist path" - where artist.nfo and art was located v17 and below. +// It is the path common to all albums by an (album) artist, but ensure it is unique +// to that artist and not shared with other artists. Previously this caused incorrect nfo +// and art to be applied to multiple artists. +bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string& basePath) +{ + basePath.clear(); + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; + + // find all albums from this artist, and all the paths to the songs from those albums + std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist " + "JOIN song ON album_artist.idAlbum = song.idAlbum " + "JOIN path ON song.idPath = path.idPath " + "WHERE album_artist.idArtist = %ld " + "GROUP BY song.idPath", + idArtist); + + // run query + if (!m_pDS2->query(strSQL)) + return false; + int iRowsFound = m_pDS2->num_rows(); + if (iRowsFound == 0) + { + // Artist is not an album artist, no path to find + m_pDS2->close(); + return false; + } + else if (iRowsFound == 1) + { + // Special case for single path - assume that we're in an artist/album/songs filesystem + URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath); + m_pDS2->close(); + } + else + { + // find the common path (if any) to these albums + while (!m_pDS2->eof()) + { + std::string path = m_pDS2->fv("strPath").get_asString(); + if (basePath.empty()) + basePath = path; + else + URIUtils::GetCommonPath(basePath, path); + + m_pDS2->next(); + } + m_pDS2->close(); + } + + // Check any path found is unique to that album artist, and do *not* return any path + // that is shared with other album artists. That could be because of collaborations + // i.e. albums with more than one album artist, or because there are albums by the + // artist on multiple music sources, or elsewhere in the folder hierarchy. + // Avoid returning some generic music path. + if (!basePath.empty()) + { + strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist " + "JOIN song ON album_artist.idAlbum = song.idAlbum " + "JOIN path ON song.idPath = path.idPath " + "WHERE album_artist.idArtist <> %ld " + "AND strPath LIKE '%s%%'", + idArtist, basePath.c_str()); + std::string strValue = GetSingleValue(strSQL, m_pDS2); + if (!strValue.empty()) + { + int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10)); + if (countartists == 0) + return true; + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + basePath.clear(); + return false; +} + +bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string& path) +{ + // Get path for artist in the artists folder + path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + if (path.empty()) + return false; // No Artists folder not set; + // Get unique artist folder name + std::string strFolder; + if (GetArtistFolderName(artist, strFolder)) + { + path = URIUtils::AddFileToFolder(path, strFolder); + return true; + } + path.clear(); + return false; +} + +bool CMusicDatabase::GetAlbumFolder(const CAlbum& album, + const std::string& strAlbumPath, + std::string& strFolder) +{ + strFolder.clear(); + // Get a name for the album folder that is unique for the artist to use when + // exporting albums to separate nfo files in a folder under an artist folder + + // When given an album path (common to all the music files containing *only* + // that album) check if that folder name is *unique* looking at folders on + // all levels of the music file paths for the artist + if (!strAlbumPath.empty()) + { + // Get last folder from full path + std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath); + if (!folders.empty()) + { + strFolder = folders.back(); + // The same folder name could be used on different paths for albums by the + // same first artist. The albums could be totally different or also have + // the same name (but different mbid). Be over cautious and look for the + // name any where in the music file paths + std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist " + "JOIN song ON album_artist.idAlbum = song.idAlbum " + "JOIN path on path.idPath = song.idPath " + "WHERE album_artist.iOrder = 0 " + "AND album_artist.idArtist = %ld " + "AND path.strPath LIKE '%%\\%s\\%%'", + album.artistCredits[0].GetArtistId(), strFolder.c_str()); + + if (!m_pDS2->query(strSQL)) + return false; + int iRowsFound = m_pDS2->num_rows(); + m_pDS2->close(); + if (iRowsFound == 1) + return true; + } + } + // Create a valid unique folder name from album title + // @todo: Does UFT8 matter or need normalizing? + // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.? + strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT); + StringUtils::Replace(strFolder, " _ ", "_"); + + // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3 + // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder. + // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name" + // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely + std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist " + "JOIN album ON album_artist.idAlbum = album.idAlbum " + "WHERE album_artist.iOrder = 0 " + "AND album_artist.idArtist = %ld " + "AND album.strAlbum LIKE '%s' ", + album.artistCredits[0].GetArtistId(), album.strAlbum.c_str()); + std::string strValue = GetSingleValue(strSQL, m_pDS2); + if (strValue.empty()) + return false; + int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10)); + if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty()) + { // Only one of the duplicate albums can be without mbid + strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4); + } + return !strFolder.empty(); +} + +bool CMusicDatabase::GetArtistFolderName(const CArtist& artist, std::string& strFolder) +{ + return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder); +} + +bool CMusicDatabase::GetArtistFolderName(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + std::string& strFolder) +{ + // Create a valid unique folder name for artist + // @todo: Does UFT8 matter or need normalizing? + // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.? + strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT); + StringUtils::Replace(strFolder, " _ ", "_"); + + // Ensure <artist name> is unique e.g. 2 x John Williams. + // To have duplicate artist names there must both have mbids, so append start of mbid to folder. + // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name" + // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely + std::string strSQL = + PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str()); + std::string strValue = GetSingleValue(strSQL, m_pDS2); + if (strValue.empty()) + return false; + int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10)); + if (countartist > 1) + strFolder += "_" + strMusicBrainzArtistID.substr(0, 4); + return !strFolder.empty(); +} + +int CMusicDatabase::AddSource(const std::string& strName, + const std::string& strMultipath, + const std::vector<std::string>& vecPaths, + int id /*= -1*/) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + // Check if source name already exists + int idSource = GetSourceByName(strName); + if (idSource < 0) + { + BeginTransaction(); + // Add new source and source paths + if (id > 0) + strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) " + "VALUES(%i, '%s', '%s')", + id, strName.c_str(), strMultipath.c_str()); + else + strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) " + "VALUES(NULL, '%s', '%s')", + strName.c_str(), strMultipath.c_str()); + m_pDS->exec(strSQL); + + idSource = static_cast<int>(m_pDS->lastinsertid()); + + int idPath = 1; + for (const auto& path : vecPaths) + { + strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) " + "VALUES(%i,%i,'%s')", + idSource, idPath, path.c_str()); + m_pDS->exec(strSQL); + ++idPath; + } + + // Find albums by song path, building WHERE for multiple source paths + // (providing source has a path) + if (vecPaths.size() > 0) + { + std::vector<int> albumIds; + Filter extFilter; + strSQL = "SELECT DISTINCT idAlbum FROM song "; + extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath"); + for (const auto& path : vecPaths) + extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false); + if (!BuildSQL(strSQL, extFilter, strSQL)) + return -1; + + if (!m_pDS->query(strSQL)) + return -1; + + while (!m_pDS->eof()) + { + albumIds.push_back(m_pDS->fv("idAlbum").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + // Add album_source for related albums + for (auto idAlbum : albumIds) + { + strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) " + "VALUES('%i', '%i')", + idSource, idAlbum); + m_pDS->exec(strSQL); + } + } + CommitTransaction(); + } + return idSource; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL); + RollbackTransaction(); + } + + return -1; +} + +int CMusicDatabase::UpdateSource(const std::string& strOldName, + const std::string& strName, + const std::string& strMultipath, + const std::vector<std::string>& vecPaths) +{ + int idSource = -1; + std::string strSourceMultipath; + std::string strSQL; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + // Get details of named old source + if (!strOldName.empty()) + { + strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'", + strOldName.c_str()); + if (!m_pDS->query(strSQL)) + return -1; + if (m_pDS->num_rows() > 0) + { + idSource = m_pDS->fv("idSource").get_asInt(); + strSourceMultipath = m_pDS->fv("strMultipath").get_asString(); + } + m_pDS->close(); + } + if (idSource < 0) + { + // Source not found, add new one + return AddSource(strName, strMultipath, vecPaths); + } + + // Nothing changed? (that we hold in db, other source details could be modified) + bool pathschanged = strMultipath.compare(strSourceMultipath) != 0; + if (!pathschanged && strOldName.compare(strName) == 0) + return idSource; + + if (!pathschanged) + { + // Name changed? Could be that none of the values held in db changed + if (strOldName.compare(strName) != 0) + { + strSQL = PrepareSQL("UPDATE source SET strName = '%s' " + "WHERE idSource = %i", + strName.c_str(), idSource); + m_pDS->exec(strSQL); + } + return idSource; + } + else + { + // Change paths (and name) by deleting and re-adding, but keep same ID + strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource); + m_pDS->exec(strSQL); + return AddSource(strName, strMultipath, vecPaths, idSource); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL); + RollbackTransaction(); + } + + return -1; +} + +bool CMusicDatabase::RemoveSource(const std::string& strName) +{ + // Related album_source and source_path rows removed by trigger + SetLibraryLastCleaned(); + return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str())); +} + +int CMusicDatabase::GetSourceFromPath(const std::string& strPath1) +{ + std::string strSQL; + int idSource = -1; + try + { + std::string strPath(strPath1); + if (!URIUtils::HasSlashAtEnd(strPath)) + URIUtils::AddSlashAtEnd(strPath); + + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + // Check if path is a source matching on multipath + strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str()); + if (!m_pDS->query(strSQL)) + return -1; + if (m_pDS->num_rows() > 0) + idSource = m_pDS->fv("idSource").get_asInt(); + m_pDS->close(); + if (idSource > 0) + return idSource; + + // Check if path is a source path (of many) or a subfolder of a single source + strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path " + "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", + strPath.c_str()); + if (!m_pDS->query(strSQL)) + return -1; + if (m_pDS->num_rows() == 1) + idSource = m_pDS->fv("idSource").get_asInt(); + m_pDS->close(); + return idSource; + } + catch (...) + { + CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath1); + } + + return -1; +} + +bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource) +{ + std::string strSQL; + strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) " + "VALUES(%i, %i)", + idAlbum, idSource); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath) +{ + std::string strSQL; + std::vector<int> sourceIds; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (!strPath.empty()) + { + // Find sources related to album using album path + strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path " + "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", + strPath.c_str()); + if (!m_pDS->query(strSQL)) + return false; + while (!m_pDS->eof()) + { + sourceIds.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + } + else + { + // Find sources using song paths, check each source path individually + if (nullptr == m_pDS2) + return false; + strSQL = "SELECT idSource, strPath FROM source_path"; + if (!m_pDS->query(strSQL)) + return false; + while (!m_pDS->eof()) + { + std::string sourcepath = m_pDS->fv("strPath").get_asString(); + strSQL = PrepareSQL("SELECT 1 FROM song " + "JOIN path ON song.idPath = path.idPath " + "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", + sourcepath.c_str()); + if (!m_pDS2->query(strSQL)) + return false; + if (m_pDS2->num_rows() > 0) + sourceIds.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS2->close(); + + m_pDS->next(); + } + m_pDS->close(); + } + + //Add album sources + for (auto idSource : sourceIds) + { + AddAlbumSource(idAlbum, idSource); + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath); + } + + return false; +} + +bool CMusicDatabase::DeleteAlbumSources(int idAlbum) +{ + return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum)); +} + +bool CMusicDatabase::CheckSources(VECSOURCES& sources) +{ + if (sources.empty()) + { + // Source table empty too? + return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty(); + } + + // Check number of entries matches + size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source")); + if (total != sources.size()) + return false; + + // Check individual sources match + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + for (const auto& source : sources) + { + // Check each source by name + strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source " + "WHERE strName LIKE '%s'", + source.strName.c_str()); + m_pDS->query(strSQL); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() != 1) + { + // Missing source, or name duplication + m_pDS->close(); + return false; + } + else + { + // Check details. Encoded URLs of source.strPath matched to strMultipath + // field, no need to look at individual paths of source_path table + if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0) + { + // Paths not match + m_pDS->close(); + return false; + } + m_pDS->close(); + } + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::MigrateSources() +{ + //Fetch music sources from xml + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music")); + + std::string strSQL; + try + { + // Fill source and source paths tables + for (const auto& source : sources) + { + // AddSource(source.strName, source.strPath, source.vecPaths); + // Add new source + strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) " + "VALUES(NULL, '%s', '%s')", + source.strName.c_str(), source.strPath.c_str()); + m_pDS->exec(strSQL); + int idSource = static_cast<int>(m_pDS->lastinsertid()); + + // Add new source paths + int idPath = 1; + for (const auto& path : source.vecPaths) + { + strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) " + "VALUES(%i,%i,'%s')", + idSource, idPath, path.c_str()); + m_pDS->exec(strSQL); + ++idPath; + } + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } + return false; +} + +bool CMusicDatabase::UpdateSources() +{ + //Check library and xml sources match + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music")); + if (CheckSources(sources)) + return true; + + try + { + // Empty sources table (related link tables removed by trigger); + ExecuteQuery("DELETE FROM source"); + + // Fill source table, and album sources + for (const auto& source : sources) + AddSource(source.strName, source.strPath, source.vecPaths); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetSources(CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Get music sources and individual source paths (may not be scanned or have albums etc.) + std::string strSQL = + "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath " + "FROM source JOIN source_path ON source.idSource = source_path.idSource " + "ORDER BY source.idSource, source_path.idPath"; + + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + // Get data from returned rows + // Item has source ID in MusicInfotag, multipath in path, and individual paths in property + CVariant sourcePaths(CVariant::VariantTypeArray); + int idSource = -1; + while (!m_pDS->eof()) + { + if (idSource != m_pDS->fv("source.idSource").get_asInt()) + { // New source + if (idSource > 0 && !sourcePaths.empty()) + { + //Store paths for previous source in item list + items[items.Size() - 1].get()->SetProperty("paths", sourcePaths); + sourcePaths.clear(); + } + idSource = m_pDS->fv("source.idSource").get_asInt(); + CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString())); + pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source"); + // Set tag URL for "file" property in AudioLibary processing + pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString()); + // Set item path as source URL encoded multipath too + pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString()); + + pItem->m_bIsFolder = true; + items.Add(pItem); + } + // Get path data + sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString()); + + m_pDS->next(); + } + if (!sourcePaths.empty()) + { + //Store paths for final source + items[items.Size() - 1].get()->SetProperty("paths", sourcePaths); + sourcePaths.clear(); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist " + "JOIN album_artist ON album_artist.idArtist = artist.idArtist " + "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum " + "WHERE artist.idArtist = %i " + "ORDER BY album_source.idSource", + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + // Artist does have any source via albums may not be an album artist. + // Check via songs fetch sources from compilations or where they are guest artist + m_pDS->close(); + strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist " + "JOIN song ON song_artist.idSong = song.idSong " + "JOIN album_source ON album_source.idAlbum = song.idAlbum " + "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 " + "ORDER BY album_source.idSource", + idArtist); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0) + { + //No sources, but query successful + m_pDS->close(); + return true; + } + } + + CVariant artistSources(CVariant::VariantTypeArray); + while (!m_pDS->eof()) + { + artistSources.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + item->SetProperty("sourceid", artistSources); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist); + } + return false; +} + +bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT idSource FROM album_source " + "WHERE album_source.idAlbum = %i " + "ORDER BY idSource", + idAlbum); + if (!m_pDS->query(strSQL)) + return false; + CVariant albumSources(CVariant::VariantTypeArray); + if (m_pDS->num_rows() > 0) + { + while (!m_pDS->eof()) + { + albumSources.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + } + else + { + //! @todo: handle singles, or don't waste time checking songs + // Album does have any sources, may be a single?? + // Check via song paths, check each source path individually + // usually fewer source paths than songs + m_pDS->close(); + + if (nullptr == m_pDS2) + return false; + strSQL = "SELECT idSource, strPath FROM source_path"; + if (!m_pDS->query(strSQL)) + return false; + while (!m_pDS->eof()) + { + std::string sourcepath = m_pDS->fv("strPath").get_asString(); + strSQL = PrepareSQL("SELECT 1 FROM song " + "JOIN path ON song.idPath = path.idPath " + "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", + idAlbum, sourcepath.c_str()); + if (!m_pDS2->query(strSQL)) + return false; + if (m_pDS2->num_rows() > 0) + albumSources.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS2->close(); + + m_pDS->next(); + } + m_pDS->close(); + } + + + item->SetProperty("sourceid", albumSources); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum); + } + return false; +} + +bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + std::string strSQL; + strSQL = PrepareSQL("SELECT idSource FROM song " + "JOIN album_source ON album_source.idAlbum = song.idAlbum " + "WHERE song.idSong = %i " + "ORDER BY idSource", + idSong); + if (!m_pDS->query(strSQL)) + return false; + if (m_pDS->num_rows() == 0 && !strPath1.empty()) + { + // Check via song path instead + m_pDS->close(); + std::string strPath(strPath1); + if (!URIUtils::HasSlashAtEnd(strPath)) + URIUtils::AddSlashAtEnd(strPath); + + strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path " + "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", + strPath.c_str()); + if (!m_pDS->query(strSQL)) + return false; + } + CVariant songSources(CVariant::VariantTypeArray); + while (!m_pDS->eof()) + { + songSources.push_back(m_pDS->fv("idSource").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + item->SetProperty("sourceid", songSources); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong); + } + return false; +} + +int CMusicDatabase::GetSourceByName(const std::string& strSource) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return -1; + } + return m_pDS->fv("idSource").get_asInt(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +std::string CMusicDatabase::GetSourceById(int id) +{ + return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id)); +} + +int CMusicDatabase::GetArtistByName(const std::string& strArtist) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'", + strArtist.c_str()); + + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return -1; + } + int lResult = m_pDS->fv("artist.idArtist").get_asInt(); + m_pDS->close(); + return lResult; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +int CMusicDatabase::GetArtistByMatch(const CArtist& artist) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + // Match on MusicBrainz ID, definitively unique + if (!artist.strMusicBrainzArtistID.empty()) + strSQL = PrepareSQL("SELECT idArtist FROM artist " + "WHERE strMusicBrainzArtistID = '%s'", + artist.strMusicBrainzArtistID.c_str()); + else + // No MusicBrainz ID, artist by name with no mbid + strSQL = PrepareSQL("SELECT idArtist FROM artist " + "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL", + artist.strArtist.c_str()); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + // Match on artist name, relax mbid restriction + return GetArtistByName(artist.strArtist); + } + int lResult = m_pDS->fv("idArtist").get_asInt(); + m_pDS->close(); + return lResult; + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL); + } + return -1; +} + +bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist& artist) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = PrepareSQL( + "SELECT artistview.* FROM song_artist " + "JOIN artistview ON song_artist.idArtist = artistview.idArtist " + "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0", + idSong); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return false; + } + + artist = GetArtistFromDataset(m_pDS.get()); + + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::IsSongArtist(int idSong, int idArtist) +{ + std::string strSQL = PrepareSQL("SELECT 1 FROM song_artist " + "WHERE song_artist.idSong= %i AND " + "song_artist.idArtist = %i AND song_artist.idRole = 1", + idSong, idArtist); + return GetSingleValue(strSQL).empty(); +} + +bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist) +{ + std::string strSQL = + PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum " + "WHERE song.idSong = %i AND album_artist.idArtist = %i", + idSong, idArtist); + return GetSingleValue(strSQL).empty(); +} + +bool CMusicDatabase::IsAlbumBoxset(int idAlbum) +{ + std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum); + int isBoxSet = GetSingleValueInt(strSQL); + return (isBoxSet == 1 ? true : false); +} + +int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + if (strArtist.empty()) + strSQL = + PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str()); + else + strSQL = PrepareSQL("SELECT idAlbum FROM album " + "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'", + strAlbum.c_str(), strArtist.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return -1; + } + return m_pDS->fv("idAlbum").get_asInt(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum, + const std::string& strArtist, + int& idAlbum, + std::string& strReview) +{ + /* + Get the first album that matches with the title and artist display name. + Artist(s) and album title may not be sufficient to uniquely identify a match since library can + store multiple releases, and occasionally artists even have different albums with same name. + Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo + */ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + if (strArtist.empty()) + strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'", + strAlbum.c_str()); + else + strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album " + "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'", + strAlbum.c_str(), strArtist.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound > 0) + { + idAlbum = m_pDS->fv("idAlbum").get_asInt(); + strReview = m_pDS->fv("strReview").get_asString(); + return true; + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + strSQL = PrepareSQL("SELECT albumview.* FROM albumview " + "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum " + "WHERE album_artist.strArtist LIKE '%s'", + strArtist.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + + while (!m_pDS->eof()) + { + CAlbum album = GetAlbumFromDataset(m_pDS.get()); + std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum); + CFileItemPtr pItem(new CFileItem(path, album)); + std::string label = + StringUtils::Format("{} ({})", album.strAlbum, pItem->GetMusicInfoTag()->GetYear()); + pItem->SetLabel(label); + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); // cleanup recordset data + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, + const std::vector<std::string>& artist) +{ + return GetAlbumByName( + strAlbum, + StringUtils::Join( + artist, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +int CMusicDatabase::GetAlbumByMatch(const CAlbum& album) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + // Match on MusicBrainz ID, definitively unique + if (!album.strMusicBrainzAlbumID.empty()) + strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'", + album.strMusicBrainzAlbumID.c_str()); + else + // No mbid, match on album title and album artist descriptive string, ignore those with mbid + strSQL = PrepareSQL("SELECT idAlbum FROM album " + "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' " + "AND strMusicBrainzAlbumID IS NULL", + album.GetAlbumArtistString().c_str(), album.strAlbum.c_str()); + m_pDS->query(strSQL); + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + // Match on album title and album artist descriptive string, relax mbid restriction + return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString()); + } + int lResult = m_pDS->fv("idAlbum").get_asInt(); + m_pDS->close(); + return lResult; + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL); + } + return -1; +} + +std::string CMusicDatabase::GetGenreById(int id) +{ + return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id)); +} + +std::string CMusicDatabase::GetArtistById(int id) +{ + return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id)); +} + +std::string CMusicDatabase::GetRoleById(int id) +{ + return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id)); +} + +bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/) +{ + // Propagate artist sort names into concatenated artist sort name string for songs and albums + // Avoid updating records where sort same as strArtistDisp + std::string strSQL; + + // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite + // (not handled by PrepareSQL) + bool bisMySQL = StringUtils::EqualsNoCase( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql"); + + BeginMultipleExecute(); + if (bisMySQL) + strSQL = "(SELECT GROUP_CONCAT(" + "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist " + "ELSE artist.strSortName END " + "ORDER BY album_artist.idAlbum, album_artist.iOrder " + "SEPARATOR '; ') as val " + "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist " + "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) "; + else + strSQL = "(SELECT GROUP_CONCAT(val, '; ') " + "FROM(SELECT album_artist.idAlbum, " + "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist " + "ELSE artist.strSortName END as val " + "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist " + "WHERE album_artist.idAlbum = album.idAlbum " + "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) "; + + strSQL = "UPDATE album SET strArtistSort = " + strSQL + + "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) " + "AND strArtistDisp <> " + + strSQL; + if (idArtist > 0) + strSQL += + PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld " + "AND album_artist.idAlbum = album.idAlbum)", + idArtist); + ExecuteQuery(strSQL); + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + if (bisMySQL) + strSQL = "(SELECT GROUP_CONCAT(" + "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist " + "ELSE artist.strSortName END " + "ORDER BY song_artist.idSong, song_artist.iOrder " + "SEPARATOR '; ') as val " + "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist " + "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) "; + else + strSQL = "(SELECT GROUP_CONCAT(val, '; ') " + "FROM(SELECT song_artist.idSong, " + "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist " + "ELSE artist.strSortName END as val " + "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist " + "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 " + "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) "; + + strSQL = "UPDATE song SET strArtistSort = " + strSQL + + "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) " + "AND strArtistDisp <> " + + strSQL; + if (idArtist > 0) + strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld " + "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)", + idArtist); + ExecuteQuery(strSQL); + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + if (CommitMultipleExecute()) + return true; + else + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + return false; +} + +std::string CMusicDatabase::GetAlbumById(int id) +{ + return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id)); +} + +int CMusicDatabase::GetGenreByName(const std::string& strGenre) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + strSQL = PrepareSQL("SELECT idGenre FROM genre " + "WHERE genre.strGenre LIKE '%s'", + strGenre.c_str()); + // run query + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound != 1) + { + m_pDS->close(); + return -1; + } + return m_pDS->fv("genre.idGenre").get_asInt(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + strSQL = "SELECT %s FROM genre "; + Filter extFilter; + extFilter.AppendField("genre.idGenre"); + extFilter.AppendField("genre.strGenre"); + if (bSources) + { + strSQL = "SELECT DISTINCT %s FROM genre "; + extFilter.AppendField("album_source.idSource"); + extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre"); + extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong"); + extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum"); + extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum"); + extFilter.AppendOrder("genre.strGenre"); + extFilter.AppendOrder("album_source.idSource"); + } + extFilter.AppendWhere("genre.strGenre != ''"); + + std::string strSQLExtra; + if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + strSQL = PrepareSQL(strSQL, extFilter.fields.c_str()) + strSQLExtra; + + // run query + CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + if (!bSources) + items.Reserve(iRowsFound); + + // Get data from returned rows + // Item has genre name and ID in MusicInfotag, VFS path, and sources in property + CVariant genreSources(CVariant::VariantTypeArray); + int idGenre = -1; + while (!m_pDS->eof()) + { + if (idGenre != m_pDS->fv("genre.idGenre").get_asInt()) + { // New genre + if (idGenre > 0 && bSources) + { + //Store sources for previous genre in item list + items[items.Size() - 1].get()->SetProperty("sourceid", genreSources); + genreSources.clear(); + } + idGenre = m_pDS->fv("genre.idGenre").get_asInt(); + std::string strGenre = m_pDS->fv("genre.strGenre").get_asString(); + CFileItemPtr pItem(new CFileItem(strGenre)); + pItem->GetMusicInfoTag()->SetTitle(strGenre); + pItem->GetMusicInfoTag()->SetGenre(strGenre); + pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre"); + pItem->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre)); + pItem->m_bIsFolder = true; + items.Add(pItem); + } + // Get source data + if (bSources) + { + int sourceid = m_pDS->fv("album_source.idSource").get_asInt(); + if (sourceid > 0) + genreSources.push_back(sourceid); + } + m_pDS->next(); + } + if (bSources) + { + //Store sources for final genre + items[items.Size() - 1].get()->SetProperty("sourceid", genreSources); + } + + // cleanup + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } + return false; +} + +std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc) +{ + // Get disc node title from ids allowing for "*all" + std::string disctitle; + std::string albumtitle; + if (idAlbum > 0) + albumtitle = GetAlbumById(idAlbum); + if (idDisc > 0) + { + disctitle = GetSingleValue("song", "strDiscSubtitle", + PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc)); + if (disctitle.empty()) + disctitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc. + if (albumtitle.empty()) + albumtitle = disctitle; + else + albumtitle = albumtitle + " - " + disctitle; + } + return albumtitle; +} + +int CMusicDatabase::GetBoxsetsCount() +{ + return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1"); +} + +int CMusicDatabase::GetAlbumDiscsCount(int idAlbum) +{ + std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum); + return GetSingleValueInt(strSQL); +} + +int CMusicDatabase::GetCompilationAlbumsCount() +{ + return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1"); +} + +int CMusicDatabase::GetSinglesCount() +{ + CDatabase::Filter filter( + PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')", + CAlbum::ReleaseTypeToString(CAlbum::Single).c_str())); + return GetSongsCount(filter); +} + +int CMusicDatabase::GetArtistCountForRole(int role) +{ + std::string strSQL = PrepareSQL( + "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role); + return GetSingleValueInt(strSQL); +} + +int CMusicDatabase::GetArtistCountForRole(const std::string& strRole) +{ + std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist " + "JOIN role ON song_artist.idRole = role.idRole " + "WHERE role.strRole LIKE '%s'", + strRole.c_str()); + return GetSingleValueInt(strSQL); +} + +bool CMusicDatabase::SetPathHash(const std::string& path, const std::string& hash) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (hash.empty()) + { // this is an empty folder - we need only add it to the path table + // if the path actually exists + if (!CDirectory::Exists(path)) + return false; + } + int idPath = AddPath(path); + if (idPath < 0) + return false; + + std::string strSQL = + PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash.c_str(), idPath); + m_pDS->exec(strSQL); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash); + } + + return false; +} + +bool CMusicDatabase::GetPathHash(const std::string& path, std::string& hash) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = PrepareSQL("select strHash from path where strPath='%s'", path.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + return false; + hash = m_pDS->fv("strHash").get_asString(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path); + } + + return false; +} + +bool CMusicDatabase::RemoveSongsFromPath(const std::string& path1, MAPSONGS& songmap, bool exact) +{ + // We need to remove all songs from this path, as their tags are going + // to be re-read. We need to remove all songs from the song table + all links to them + // from the song link tables (as otherwise if a song is added back + // to the table with the same idSong, these tables can't be cleaned up properly later) + + //! @todo SQLite probably doesn't allow this, but can we rely on that?? + + // We don't need to remove orphaned albums at this point as in AddAlbum() we check + // first whether the album has already been read during this scan, and if it hasn't + // we check whether it's in the table and update accordingly at that point, removing the entries from + // the album link tables. The only failure point for this is albums + // that span multiple folders, where just the files in one folder have been changed. In this case + // any linked fields that are only in the files that haven't changed will be removed. Clearly + // the primary albumartist still matches (as that's what we looked up based on) so is this really + // an issue? I don't think it is, as those artists will still have links to the album via the songs + // which is generally what we rely on, so the only failure point is albumartist lookup. In this + // case, it will return only things in the album_artist table from the newly updated songs (and + // only if they have additional artists). I think the effect of this is minimal at best, as ALL + // songs in the album should have the same albumartist! + + // we also remove the path at this point as it will be added later on if the + // path still exists. + // After scanning we then remove the orphaned artists, genres and thumbs. + + // Note: when used to remove all songs from a path and its subpath (exact=false), this + // does miss archived songs. + std::string path(path1); + SetLibraryLastUpdated(); + try + { + if (!URIUtils::HasSlashAtEnd(path)) + URIUtils::AddSlashAtEnd(path); + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // Filename is not unique for a path as songs from a cuesheet have same filename. + // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet + // in a folder and some edited and rescanned. + // Hence order by filename so these songs can be gathered together. + std::string where; + if (exact) + where = PrepareSQL(" WHERE strPath='%s'", path.c_str()); + else + where = PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()), + path.c_str()); + std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName"; + if (!m_pDS->query(sql)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound > 0) + { + // Each file is potentially mapped to a list of songs, gather these and save as list + VECSONGS songs; + std::string filename; + std::vector<std::string> songIds; + while (!m_pDS->eof()) + { + CSong song = GetSongFromDataset(); + if (!filename.empty() && filename != song.strFileName) + { + // Save songs for previous filename + songmap.insert(std::make_pair(filename, songs)); + songs.clear(); + } + song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb"); + songs.emplace_back(song); + songIds.push_back(PrepareSQL("%i", song.idSong)); + filename = song.strFileName; + + m_pDS->next(); + } + m_pDS->close(); + songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename + + //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore + for (const auto& id : songIds) + AnnounceRemove(MediaTypeSong, atoi(id.c_str())); + + // Delete all songs, and anything linked to them via triggers + std::string strIDs = StringUtils::Join(songIds, ","); + sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")"; + m_pDS->exec(sql); + } + // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty) + sql = "delete from path" + where; + m_pDS->exec(sql); + return iRowsFound > 0; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path); + } + return false; +} + +void CMusicDatabase::CheckArtistLinksChanged() +{ + std::string strSQL = "SELECT COUNT(1) FROM removed_link "; + int iLinks = GetSingleValueInt(strSQL, m_pDS); + if (iLinks > 0) + { + SetArtistLinksUpdated(); // Store datetime artist links last updated + DeleteRemovedLinks(); // Clean-up artist links + } +} + +bool CMusicDatabase::GetPaths(std::set<std::string>& paths) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + paths.clear(); + + // find all paths + if (!m_pDS->query("SELECT strPath FROM path")) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + while (!m_pDS->eof()) + { + paths.insert(m_pDS->fv("strPath").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CMusicDatabase::SetSongUserrating(const std::string& filePath, int userrating) +{ + try + { + if (filePath.empty()) + return false; + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int songID = GetSongIDFromPath(filePath); + if (-1 == songID) + return false; + + return SetSongUserrating(songID, userrating); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, userrating); + } + return false; +} + +bool CMusicDatabase::SetSongUserrating(int idSong, int userrating) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql = + PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong); + m_pDS->exec(sql); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idSong, userrating); + } + return false; +} + +bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (-1 == idAlbum) + return false; + std::string sql = + PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum); + m_pDS->exec(sql); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idAlbum, userrating); + } + return false; +} + +bool CMusicDatabase::SetSongVotes(const std::string& filePath, int votes) +{ + try + { + if (filePath.empty()) + return false; + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int songID = GetSongIDFromPath(filePath); + if (-1 == songID) + return false; + + std::string sql = PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID); + + m_pDS->exec(sql); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, votes); + } + return false; +} + +int CMusicDatabase::GetSongIDFromPath(const std::string& filePath) +{ + // grab the where string to identify the song id + CURL url(filePath); + if (url.IsProtocol("musicdb")) + { + std::string strFile = URIUtils::GetFileName(filePath); + URIUtils::RemoveExtension(strFile); + return atoi(strFile.c_str()); + } + // hit the db + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strPath, strFileName; + SplitPath(filePath, strPath, strFileName); + URIUtils::AddSlashAtEnd(strPath); + + std::string sql = PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath " + "WHERE song.strFileName='%s' AND path.strPath='%s'", + strFileName.c_str(), strPath.c_str()); + if (!m_pDS->query(sql)) + return -1; + + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return -1; + } + + int songID = m_pDS->fv("idSong").get_asInt(); + m_pDS->close(); + return songID; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + return -1; +} + +bool CMusicDatabase::CommitTransaction() +{ + if (CDatabase::CommitTransaction()) + { // number of items in the db has likely changed, so reset the infomanager cache + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + { + gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool( + LIBRARY_HAS_MUSIC, GetSongsCount() > 0); + return true; + } + } + return false; +} + +bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + std::string strSQL; + int idSetting = -1; + try + { + CONTENT_TYPE content = CONTENT_NONE; + + // Build where clause from virtual path + Filter extFilter; + CMusicDbUrl musicUrl; + SortDescription sorting; + if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting)) + return false; + + std::string itemType = musicUrl.GetType(); + if (StringUtils::EqualsNoCase(itemType, "artists")) + { + content = CONTENT_ARTISTS; + } + else if (StringUtils::EqualsNoCase(itemType, "albums")) + { + content = CONTENT_ALBUMS; + } + else + return false; //Only artists and albums have info settings + + std::string strSQLWhere; + if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere)) + return false; + + // Replace view names with table names + StringUtils::Replace(strSQLWhere, "artistview", "artist"); + StringUtils::Replace(strSQLWhere, "albumview", "album"); + + BeginTransaction(); + // Clear current scraper settings (0 => default scraper used) + if (content == CONTENT_ARTISTS) + strSQL = "UPDATE artist SET idInfoSetting = %i "; + else + strSQL = "UPDATE album SET idInfoSetting = %i "; + strSQL = PrepareSQL(strSQL, 0) + strSQLWhere; + m_pDS->exec(strSQL); + + //Remove orphaned settings + CleanupInfoSettings(); + + if (scraper) + { + // Add new info setting + strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')"; + strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str()); + m_pDS->exec(strSQL); + idSetting = static_cast<int>(m_pDS->lastinsertid()); + + if (content == CONTENT_ARTISTS) + strSQL = "UPDATE artist SET idInfoSetting = %i "; + else + strSQL = "UPDATE album SET idInfoSetting = %i "; + strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere; + m_pDS->exec(strSQL); + } + CommitTransaction(); + return true; + } + catch (...) + { + RollbackTransaction(); + CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, strBaseDir, strSQL); + } + return false; +} + +bool CMusicDatabase::SetScraper(int id, + const CONTENT_TYPE& content, + const ADDON::ScraperPtr& scraper) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + std::string strSQL; + int idSetting = -1; + try + { + BeginTransaction(); + // Fetch current info settings for item, 0 => default is used + if (content == CONTENT_ARTISTS) + strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i"; + else + strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i"; + strSQL = PrepareSQL(strSQL, id); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + idSetting = m_pDS->fv("idInfoSetting").get_asInt(); + m_pDS->close(); + + if (idSetting < 1) + { // Add new info setting + strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')"; + strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str()); + m_pDS->exec(strSQL); + idSetting = static_cast<int>(m_pDS->lastinsertid()); + + if (content == CONTENT_ARTISTS) + strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i"; + else + strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i"; + strSQL = PrepareSQL(strSQL, idSetting, id); + m_pDS->exec(strSQL); + } + else + { // Update info setting + strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' " + "WHERE idSetting = %i"; + strSQL = + PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting); + m_pDS->exec(strSQL); + } + CommitTransaction(); + return true; + } + catch (...) + { + RollbackTransaction(); + CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, id, strSQL); + } + return false; +} + +bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper) +{ + std::string scraperUUID; + std::string strSettings; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN "; + if (content == CONTENT_ARTISTS) + strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting " + "WHERE artist.idArtist = %i"; + else + strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting " + "WHERE album.idAlbum = %i"; + strSQL = PrepareSQL(strSQL, id); + m_pDS->query(strSQL); + if (!m_pDS->eof()) + { // try and ascertain scraper + scraperUUID = m_pDS->fv("strScraperPath").get_asString(); + strSettings = m_pDS->fv("strSettings").get_asString(); + + // Use pre configured or default scraper + ADDON::AddonPtr addon; + if (!scraperUUID.empty() && + CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon, + ADDON::OnlyEnabled::CHOICE_YES) && + addon) + { + scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon); + if (scraper) + // Set settings + scraper->SetPathSettings(content, strSettings); + } + } + m_pDS->close(); + + if (!scraper) + { // use default music scraper instead + ADDON::AddonPtr addon; + if (ADDON::CAddonSystemSettings::GetInstance().GetActive( + ADDON::ScraperTypeFromContent(content), addon)) + { + scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon); + return scraper != NULL; + } + else + return false; + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} -({}, {} {}) failed", __FUNCTION__, id, scraperUUID, strSettings); + } + return false; +} + +bool CMusicDatabase::ScraperInUse(const std::string& scraperID) const +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql = + PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID.c_str()); + if (!m_pDS->query(sql) || m_pDS->num_rows() == 0) + { + m_pDS->close(); + return false; + } + bool found = m_pDS->fv(0).get_asInt() > 0; + m_pDS->close(); + return found; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID); + } + return false; +} + +bool CMusicDatabase::GetItems(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter /* = Filter() */, + const SortDescription& sortDescription /* = SortDescription() */) +{ + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(strBaseDir)) + return false; + + return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription); +} + +bool CMusicDatabase::GetItems(const std::string& strBaseDir, + const std::string& itemType, + CFileItemList& items, + const Filter& filter /* = Filter() */, + const SortDescription& sortDescription /* = SortDescription() */) +{ + if (StringUtils::EqualsNoCase(itemType, "genres")) + return GetGenresNav(strBaseDir, items, filter); + else if (StringUtils::EqualsNoCase(itemType, "sources")) + return GetSourcesNav(strBaseDir, items, filter); + else if (StringUtils::EqualsNoCase(itemType, "years")) + return GetYearsNav(strBaseDir, items, filter); + else if (StringUtils::EqualsNoCase(itemType, "roles")) + return GetRolesNav(strBaseDir, items, filter); + else if (StringUtils::EqualsNoCase(itemType, "artists")) + return GetArtistsNav(strBaseDir, items, + !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), + -1, -1, -1, filter, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "albums")) + return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "discs")) + return GetDiscsByWhere(strBaseDir, filter, items, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "songs")) + return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true); + + return false; +} + +std::string CMusicDatabase::GetItemById(const std::string& itemType, int id) +{ + if (StringUtils::EqualsNoCase(itemType, "genres")) + return GetGenreById(id); + else if (StringUtils::EqualsNoCase(itemType, "sources")) + return GetSourceById(id); + else if (StringUtils::EqualsNoCase(itemType, "years")) + return std::to_string(id); + else if (StringUtils::EqualsNoCase(itemType, "artists")) + return GetArtistById(id); + else if (StringUtils::EqualsNoCase(itemType, "albums")) + return GetAlbumById(id); + else if (StringUtils::EqualsNoCase(itemType, "roles")) + return GetRoleById(id); + + return ""; +} + +void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, + CGUIDialogProgress* progressDialog /*= nullptr*/) +{ + if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) && + !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) && + !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) && + !settings.IsItemExported(ELIBEXPORT_ALBUMS) && !settings.IsItemExported(ELIBEXPORT_SONGS)) + return; + + // Exporting albums either art or NFO (or both) selected + if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && settings.m_skipnfo && + !settings.m_artwork && settings.IsItemExported(ELIBEXPORT_ALBUMS)) + return; + + std::string strFolder; + if (settings.IsSingleFile() || settings.IsSeparateFiles()) + { + // Exporting to single file or separate files in a specified location + if (settings.m_strPath.empty()) + return; + + strFolder = settings.m_strPath; + if (!URIUtils::HasSlashAtEnd(strFolder)) + URIUtils::AddSlashAtEnd(strFolder); + strFolder = URIUtils::GetDirectory(strFolder); + if (strFolder.empty()) + return; + } + else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists())) + { + // Exporting artist folders only, or artist NFO or art to library folders + // need Artist Information Folder defined. + // (Album NFO and art goes to music folders) + strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + if (strFolder.empty()) + return; + } + + // + bool artistfoldersonly; + artistfoldersonly = settings.IsArtistFoldersOnly() || + ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && + settings.m_skipnfo && !settings.m_artwork); + + int iFailCount = 0; + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + if (nullptr == m_pDS2) + return; + + // Create our xml document + CXBMCTinyXML xmlDoc; + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + TiXmlNode* pMain = NULL; + if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly) + pMain = &xmlDoc; + else if (settings.IsSingleFile()) + { + TiXmlElement xmlMainElement("musicdb"); + pMain = xmlDoc.InsertEndChild(xmlMainElement); + } + + if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly) + { + // Find albums to export + std::vector<int> albumIds; + std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ", + CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()); + if (!settings.m_unscraped) + strSQL += "AND lastScraped IS NOT NULL"; + CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL); + m_pDS->query(strSQL); + + int total = m_pDS->num_rows(); + int current = 0; + + albumIds.reserve(total); + while (!m_pDS->eof()) + { + albumIds.push_back(m_pDS->fv("idAlbum").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + for (const auto& albumId : albumIds) + { + CAlbum album; + GetAlbum(albumId, album); + std::string strAlbumPath; + std::string strPath; + // Get album path, empty unless all album songs are under a unique folder, and + // there are no songs from another album in the same folder. + if (!GetAlbumPath(albumId, strAlbumPath)) + strAlbumPath.clear(); + if (settings.IsSingleFile()) + { + // Save album to xml, including album path + album.Save(pMain, "album", strAlbumPath); + } + else + { // Separate files and artwork + bool pathfound = false; + if (settings.IsToLibFolders()) + { // Save album.nfo and artwork with music files. + // Most albums are under a unique folder, but if songs from various albums are mixed then + // avoid overwriting by not allow NFO and art to be exported + if (strAlbumPath.empty()) + CLog::Log(LOGDEBUG, + "CMusicDatabase::{} - Not exporting album {} as unique path not found", + __FUNCTION__, album.strAlbum); + else if (!CDirectory::Exists(strAlbumPath)) + CLog::Log( + LOGDEBUG, + "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist", + __FUNCTION__, album.strAlbum, strAlbumPath); + else + { + strPath = strAlbumPath; + pathfound = true; + } + } + else + { // Save album.nfo and artwork to subfolder on export path + // strPath = strFolder/<albumartist name>/<albumname> + // where <albumname> is either the same name as the album folder + // containing the music files (if unique) or is created using the album name + std::string strAlbumArtist; + pathfound = GetArtistFolderName(album.GetAlbumArtist()[0], + album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist); + if (pathfound) + { + strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist); + pathfound = CDirectory::Exists(strPath); + if (!pathfound) + pathfound = CDirectory::Create(strPath); + } + if (!pathfound) + CLog::Log(LOGDEBUG, + "CMusicDatabase::{} - Not exporting album {} as could not create {}", + __FUNCTION__, album.strAlbum, strPath); + else + { + std::string strAlbumFolder; + pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder); + if (pathfound) + { + strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder); + pathfound = CDirectory::Exists(strPath); + if (!pathfound) + pathfound = CDirectory::Create(strPath); + } + if (!pathfound) + CLog::Log(LOGDEBUG, + "CMusicDatabase::{} - Not exporting album {} as could not create {}", + __FUNCTION__, album.strAlbum, strPath); + } + } + if (pathfound) + { + if (!settings.m_skipnfo) + { + // Save album to NFO, including album path + album.Save(pMain, "album", strAlbumPath); + std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo"); + if (settings.m_overwrite || !CFile::Exists(nfoFile)) + { + if (!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "CMusicDatabase::{}: Album nfo export failed! ('{}')", + __FUNCTION__, nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + if (settings.m_artwork) + { + // Save art in album folder + // Note thumb resolution may be lower than original when overwriting + std::map<std::string, std::string> artwork; + std::string savedArtfile; + if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork)) + { + for (const auto& art : artwork) + { + if (art.first == "thumb") + savedArtfile = URIUtils::AddFileToFolder(strPath, "folder"); + else + savedArtfile = URIUtils::AddFileToFolder(strPath, art.first); + CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile, + settings.m_overwrite); + } + } + } + xmlDoc.Clear(); + xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes") + } + } + + if ((current % 50) == 0 && progressDialog) + { + progressDialog->SetLine(1, CVariant{album.strAlbum}); + progressDialog->SetPercentage(current * 100 / total); + if (progressDialog->IsCanceled()) + return; + } + current++; + } + } + + // Export song playback history to single file only + if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS)) + { + if (!ExportSongHistory(pMain, progressDialog)) + return; + } + + if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty()) + { + // Find artists to export + std::vector<int> artistIds; + Filter filter; + + if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS)) + filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist " + "WHERE album_artist.idArtist = artist.idArtist)", + false); + if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS)) + { + if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS)) + filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist " + "WHERE song_artist.idArtist = artist.idArtist )", + false); + else + filter.AppendWhere( + "EXISTS (SELECT 1 FROM song_artist " + "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)", + false); + } + else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS)) + filter.AppendWhere( + "EXISTS (SELECT 1 FROM song_artist " + "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)", + false); + + if (!settings.m_unscraped && !artistfoldersonly) + filter.AppendWhere("lastScraped IS NOT NULL", true); + + std::string strSQL = "SELECT idArtist FROM artist"; + BuildSQL(strSQL, filter, strSQL); + CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL); + + m_pDS->query(strSQL); + int total = m_pDS->num_rows(); + int current = 0; + artistIds.reserve(total); + while (!m_pDS->eof()) + { + artistIds.push_back(m_pDS->fv("idArtist").get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + + for (const auto& artistId : artistIds) + { + CArtist artist; + // Include discography when not folders only + GetArtist(artistId, artist, !artistfoldersonly); + std::string strPath; + std::map<std::string, std::string> artwork; + if (settings.IsSingleFile()) + { + // Save artist to xml, and old path (common to music files) if it has one + GetOldArtistPath(artist.idArtist, strPath); + artist.Save(pMain, "artist", strPath); + + if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork)) + { // append to the XML + TiXmlElement additionalNode("art"); + for (const auto& i : artwork) + XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second); + pMain->LastChild()->InsertEndChild(additionalNode); + } + } + else + { // Separate files: artist.nfo and artwork in strFolder/<artist name> + // Get unique folder allowing for duplicate names e.g. 2 x John Williams + bool pathfound = GetArtistFolderName(artist, strPath); + if (pathfound) + { + strPath = URIUtils::AddFileToFolder(strFolder, strPath); + pathfound = CDirectory::Exists(strPath); + if (!pathfound) + pathfound = CDirectory::Create(strPath); + } + if (!pathfound) + CLog::Log(LOGDEBUG, + "CMusicDatabase::{} - Not exporting artist {} as could not create {}", + __FUNCTION__, artist.strArtist, strPath); + else + { + if (!artistfoldersonly) + { + if (!settings.m_skipnfo) + { + artist.Save(pMain, "artist", strPath); + std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo"); + if (settings.m_overwrite || !CFile::Exists(nfoFile)) + { + if (!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "CMusicDatabase::{}: Artist nfo export failed! ('{}')", + __FUNCTION__, nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + if (settings.m_artwork) + { + std::string savedArtfile; + if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork)) + { + for (const auto& art : artwork) + { + if (art.first == "thumb") + savedArtfile = URIUtils::AddFileToFolder(strPath, "folder"); + else + savedArtfile = URIUtils::AddFileToFolder(strPath, art.first); + CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile, + settings.m_overwrite); + } + } + } + xmlDoc.Clear(); + xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes") + } + } + } + if ((current % 50) == 0 && progressDialog) + { + progressDialog->SetLine(1, CVariant{artist.strArtist}); + progressDialog->SetPercentage(current * 100 / total); + if (progressDialog->IsCanceled()) + return; + } + current++; + } + } + + if (settings.IsSingleFile()) + { + std::string xmlFile = URIUtils::AddFileToFolder( + strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml"); + if (CFile::Exists(xmlFile)) + xmlFile = URIUtils::AddFileToFolder( + strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml"); + xmlDoc.SaveFile(xmlFile); + + CVariant data; + data["file"] = xmlFile; + if (iFailCount > 0) + data["failcount"] = iFailCount; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport", + data); + } + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{} failed", __FUNCTION__); + iFailCount++; + } + + if (progressDialog) + progressDialog->Close(); + + if (iFailCount > 0 && progressDialog) + HELPERS::ShowOKDialogLines( + CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)}); +} + +bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog) +{ + try + { + // Export songs with some playback history + std::string strSQL = + "SELECT idSong, song.idAlbum, " + "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, " + "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, " + "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating " + "FROM song JOIN album on album.idAlbum = song.idAlbum " + "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0"; + + CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL); + m_pDS->query(strSQL); + + int total = m_pDS->num_rows(); + int current = 0; + while (!m_pDS->eof()) + { + TiXmlElement songElement("song"); + TiXmlNode* song = pNode->InsertEndChild(songElement); + + XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt()); + XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString()); + XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString()); + XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt()); + XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString()); + XMLUtils::SetString(song, "musicbrainztrackid", + m_pDS->fv("strMusicBrainzTrackID").get_asString()); + XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt()); + XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString()); + XMLUtils::SetString(song, "musicbrainzalbumid", + m_pDS->fv("strMusicBrainzAlbumID").get_asString()); + XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString()); + XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt()); + XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString()); + auto* rating = XMLUtils::SetString( + song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat())); + if (rating) + rating->ToElement()->SetAttribute("max", 10); + XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt()); + auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt()); + if (userrating) + userrating->ToElement()->SetAttribute("max", 10); + + if ((current % 100) == 0 && progressDialog) + { + progressDialog->SetLine(1, CVariant{m_pDS->fv("strAlbum").get_asString()}); + progressDialog->SetPercentage(current * 100 / total); + if (progressDialog->IsCanceled()) + { + m_pDS->close(); + return false; + } + } + current++; + + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{0} failed", __FUNCTION__); + } + return false; +} + +void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(xmlFile) && progressDialog) + { + HELPERS::ShowOKDialogLines(CVariant{20197}, CVariant{38354}); //"Unable to read xml file" + return; + } + + TiXmlElement* root = xmlDoc.RootElement(); + if (!root) + return; + + TiXmlElement* entry = root->FirstChildElement(); + int current = 0; + int total = 0; + int songtotal = 0; + // Count the number of artists, albums and songs + while (entry) + { + if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 || + StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0) + total++; + else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0) + songtotal++; + + entry = entry->NextSiblingElement(); + } + + BeginTransaction(); + entry = root->FirstChildElement(); + while (entry) + { + std::string strTitle; + if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0) + { + CArtist importedArtist; + importedArtist.Load(entry); + strTitle = importedArtist.strArtist; + + // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name + int idArtist = GetArtistByMatch(importedArtist); + if (idArtist > -1) + { + CArtist artist; + GetArtist(idArtist, artist, true); // include discography + artist.MergeScrapedArtist(importedArtist, true); + UpdateArtist(artist); + } + else + CLog::Log(LOGDEBUG, "{} - Not import additional artist data as {} not found", + __FUNCTION__, importedArtist.strArtist); + current++; + } + else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0) + { + CAlbum importedAlbum; + importedAlbum.Load(entry); + strTitle = importedAlbum.strAlbum; + // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist + int idAlbum = GetAlbumByMatch(importedAlbum); + if (idAlbum > -1) + { + CAlbum album; + GetAlbum(idAlbum, album, true); + album.MergeScrapedAlbum(importedAlbum, true); + UpdateAlbum(album); //Will replace song artists if present in xml + } + else + CLog::Log(LOGDEBUG, "{} - Not import additional album data as {} not found", __FUNCTION__, + importedAlbum.strAlbum); + + current++; + } + entry = entry->NextSiblingElement(); + if (progressDialog && total) + { + progressDialog->SetPercentage(current * 100 / total); + progressDialog->SetLine(2, CVariant{std::move(strTitle)}); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + RollbackTransaction(); + return; + } + } + } + CommitTransaction(); + + // Import song playback history <song> entries found + if (songtotal > 0) + if (!ImportSongHistory(xmlFile, songtotal, progressDialog)) + return; + + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + RollbackTransaction(); + } + if (progressDialog) + progressDialog->Close(); +} + +bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile, + const int total, + CGUIDialogProgress* progressDialog) +{ + bool bHistSongExists = false; + try + { + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(xmlFile)) + return false; + + TiXmlElement* root = xmlDoc.RootElement(); + if (!root) + return false; + + TiXmlElement* entry = root->FirstChildElement(); + int current = 0; + + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history" + progressDialog->SetLine(2, CVariant{""}); + } + + // As can be many songs do in db, not song at a time which would be slow + // Convert xml entries into a SQL bulk insert statement + std::string strSQL; + entry = root->FirstChildElement(); + while (entry) + { + std::string strArtistDisp; + std::string strTitle; + int iTrack; + std::string strFilename; + std::string strMusicBrainzTrackID; + std::string strAlbum; + std::string strMusicBrainzAlbumID; + std::string strAlbumArtistDisp; + int iTimesplayed; + std::string lastplayed; + int iUserrating = 0; + float fRating = 0.0; + int iVotes; + std::string strSQLSong; + if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0) + { + XMLUtils::GetString(entry, "artistdesc", strArtistDisp); + XMLUtils::GetString(entry, "title", strTitle); + XMLUtils::GetInt(entry, "track", iTrack); + XMLUtils::GetString(entry, "filename", strFilename); + XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID); + XMLUtils::GetString(entry, "albumtitle", strAlbum); + XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID); + XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp); + XMLUtils::GetInt(entry, "timesplayed", iTimesplayed); + XMLUtils::GetString(entry, "lastplayed", lastplayed); + const TiXmlElement* rElement = entry->FirstChildElement("rating"); + if (rElement) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(entry, "rating", rating); + if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1) + rating *= (10.f / max_rating); // Normalise the value to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + fRating = rating; + } + XMLUtils::GetInt(entry, "votes", iVotes); + const TiXmlElement* userrating = entry->FirstChildElement("userrating"); + if (userrating) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(entry, "userrating", rating); + if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && + max_rating >= 1) + rating *= (10.f / max_rating); // Normalise the value to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + iUserrating = MathUtils::round_int(static_cast<double>(rating)); + } + + strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack); + strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(), + strFilename.c_str()); + if (strMusicBrainzTrackID.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str()); + strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str()); + if (strMusicBrainzAlbumID.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str()); + strSQLSong += PrepareSQL("%d, ", iTimesplayed); + if (lastplayed.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str()); + strSQLSong += + PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating), iVotes, iUserrating); + + if (current > 0) + strSQLSong = ", " + strSQLSong; + strSQL += strSQLSong; + current++; + } + + entry = entry->NextSiblingElement(); + + if ((current % 100) == 0 && progressDialog) + { + progressDialog->SetPercentage(current * 100 / total); + progressDialog->SetLine(3, CVariant{std::move(strTitle)}); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + return false; + } + } + + CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__, + total); + /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of + song table using correlated subqueries to a temp table. An updatable join + to temp table would work in MySQL but SQLite not support updatable joins. + */ + m_pDS->exec("CREATE TABLE HistSong (" + "idSongSrc INTEGER primary key, " + "strAlbum varchar(256), " + "strMusicBrainzAlbumID text, " + "strAlbumArtistDisp text, " + "strArtistDisp text, strTitle varchar(512), " + "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, " + "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, " + "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, " + "userrating INTEGER NOT NULL DEFAULT 0, " + "idAlbum INTEGER, idSong INTEGER)"); + bHistSongExists = true; + + strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, " + "strFileName, strMusicBrainzTrackID, " + "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, " + " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " + + strSQL; + m_pDS->exec(strSQL); + + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{38351}); //"Matching data" + progressDialog->SetLine(3, CVariant{""}); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + BeginTransaction(); + // Match albums first on mbid then artist string and album title, setting idAlbum + // mbid is unique so subquery can only return one result at most + strSQL = "UPDATE HistSong " + "SET idAlbum = (SELECT album.idAlbum FROM album " + "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) " + "WHERE EXISTS(SELECT 1 FROM album " + "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0"; + m_pDS->exec(strSQL); + + // Can only be one album with same title and artist(s) and no mbid. + // But could have 2 releases one with and one without mbid, match up those without mbid + strSQL = "UPDATE HistSong " + "SET idAlbum = (SELECT album.idAlbum FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp " + "AND HistSong.strAlbum = album.strAlbum " + "AND album.strMusicBrainzAlbumID IS NULL " + "AND HistSong.strMusicBrainzAlbumID IS NULL) " + "WHERE EXISTS(SELECT 1 FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp " + "AND HistSong.strAlbum = album.strAlbum " + "AND album.strMusicBrainzAlbumID IS NULL " + "AND HistSong.strMusicBrainzAlbumID IS NULL) " + "AND idAlbum < 0"; + m_pDS->exec(strSQL); + + // Try match rest by title and artist(s), prioritise one without mbid + // Target could have multiple releases - with mbid (non-matching) or one without mbid + strSQL = "UPDATE HistSong " + "SET idAlbum = (SELECT album.idAlbum FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp " + "AND HistSong.strAlbum = album.strAlbum " + "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) " + "WHERE EXISTS(SELECT 1 FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp " + "AND HistSong.strAlbum = album.strAlbum) " + "AND idAlbum < 0"; + m_pDS->exec(strSQL); + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + RollbackTransaction(); + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong + strSQL = "UPDATE HistSong " + "SET idSong = (SELECT idsong FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND " + "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) " + "WHERE EXISTS(SELECT 1 FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND " + "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0"; + m_pDS->exec(strSQL); + + // An album can have more than one song with same track and title (although idAlbum, track and + // title is often unique), but not using filename as an identifier to allow for import of song + // history for renamed files. It is about song playback not file playback. + // Pick the first + strSQL = "UPDATE HistSong " + "SET idSong = (SELECT idsong FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) " + "WHERE EXISTS(SELECT 1 FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0"; + m_pDS->exec(strSQL); + + CommitTransaction(); + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + // Create an index to speed up the updates + m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)"); + + // Log how many songs matched + int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS); + CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__, + total - unmatched, total); + + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history" + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); // Drops index too + return false; + } + } + + /* Update song table using the song ids we have matched. + Use correlated subqueries as SQLite does not support updatable joins. + MySQL requires HistSong table not to be defined temporary for this. + */ + + BeginTransaction(); + // Times played and last played date(when count is greater) + strSQL = "UPDATE song SET iTimesPlayed = " + "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), " + "lastplayed = " + "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)"; + m_pDS->exec(strSQL); + + // User rating + strSQL = "UPDATE song SET userrating = " + "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.userrating > 0)"; + m_pDS->exec(strSQL); + + // Rating and votes + strSQL = "UPDATE song SET rating = " + "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), " + "votes = " + "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.rating > 0)"; + m_pDS->exec(strSQL); + + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + RollbackTransaction(); + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + CommitTransaction(); + + // Tidy up temp table (index also removed) + m_pDS->exec("DROP TABLE HistSong"); + // Compact db to recover space as had to add/drop actual table + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{331}); + progressDialog->Progress(); + } + Compress(false); + + // Write event log entry + // "Importing song history {1} of {2} songs matched", total - unmatched, total) + std::string strLine = + StringUtils::Format(g_localizeStrings.Get(38353), total - unmatched, total); + + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information))); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + RollbackTransaction(); + if (bHistSongExists) + m_pDS->exec("DROP TABLE HistSong"); + } + return false; +} + +void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist) +{ + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + item.SetProperty("artist_sortname", artist.strSortName); + item.SetProperty("artist_type", artist.strType); + item.SetProperty("artist_gender", artist.strGender); + item.SetProperty("artist_disambiguation", artist.strDisambiguation); + item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator)); + item.SetProperty("artist_instrument_array", artist.instruments); + item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator)); + item.SetProperty("artist_style_array", artist.styles); + item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator)); + item.SetProperty("artist_mood_array", artist.moods); + item.SetProperty("artist_born", artist.strBorn); + item.SetProperty("artist_formed", artist.strFormed); + item.SetProperty("artist_description", artist.strBiography); + item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator)); + item.SetProperty("artist_genre_array", artist.genre); + item.SetProperty("artist_died", artist.strDied); + item.SetProperty("artist_disbanded", artist.strDisbanded); + item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator)); + item.SetProperty("artist_yearsactive_array", artist.yearsActive); +} + +void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album) +{ + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + item.SetProperty("album_description", album.strReview); + item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator)); + item.SetProperty("album_theme_array", album.themes); + item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator)); + item.SetProperty("album_mood_array", album.moods); + item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator)); + item.SetProperty("album_style_array", album.styles); + item.SetProperty("album_type", album.strType); + item.SetProperty("album_label", album.strLabel); + item.SetProperty("album_artist", album.GetAlbumArtistString()); + item.SetProperty("album_artist_array", album.GetAlbumArtist()); + item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator)); + item.SetProperty("album_genre_array", album.genre); + item.SetProperty("album_title", album.strAlbum); + if (album.fRating > 0) + item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating)); + if (album.iUserrating > 0) + item.SetProperty("album_userrating", album.iUserrating); + if (album.iVotes > 0) + item.SetProperty("album_votes", album.iVotes); + + item.SetProperty("album_isboxset", album.bBoxedSet); + item.SetProperty("album_totaldiscs", album.iTotalDiscs); + item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType)); + item.SetProperty("album_duration", + StringUtils::SecondsToTimeString(album.iAlbumDuration, + static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS))); +} + +void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item) +{ + if (!item.HasMusicInfoTag()) + return; + // May already have song artist ids as item property set when data read from + // db, but check property is valid array (scripts could set item properties + // incorrectly), otherwise try to fetch artist by name. + int idArtist = -1; + if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray()) + { + CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array(); + idArtist = static_cast<int>(varid->asInteger()); + } + else + idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString()); + if (idArtist > -1) + { + CArtist artist; + if (GetArtist(idArtist, artist)) + SetPropertiesFromArtist(item, artist); + } + int idAlbum = item.GetMusicInfoTag()->GetAlbumId(); + if (idAlbum <= 0) + idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(), + item.GetMusicInfoTag()->GetArtistString()); + if (idAlbum > -1) + { + CAlbum album; + if (GetAlbum(idAlbum, album, false)) + SetPropertiesFromAlbum(item, album); + } +} + +void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType) +{ + std::string strSQL; + try + { + if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong) + return; + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field + // e.g. when artwork for item is changed from info dialog but not item details. + // Use SQL UPDATE that does not change record data. + if (mediaType == MediaTypeArtist) + strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId); + else if (mediaType == MediaTypeAlbum) + strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId); + else // MediaTypeSong + strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__, + mediaId, mediaType, strSQL); + } +} + +void CMusicDatabase::SetArtForItem(int mediaId, + const std::string& mediaType, + const std::map<std::string, std::string>& art) +{ + for (const auto& i : art) + SetArtForItem(mediaId, mediaType, i.first, i.second); +} + +void CMusicDatabase::SetArtForItem(int mediaId, + const std::string& mediaType, + const std::string& artType, + const std::string& url) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + // don't set <foo>.<bar> art types - these are derivative types from parent items + if (artType.find('.') != std::string::npos) + return; + + std::string sql = PrepareSQL("SELECT art_id FROM art " + "WHERE media_id=%i AND media_type='%s' AND type='%s'", + mediaId, mediaType.c_str(), artType.c_str()); + m_pDS->query(sql); + if (!m_pDS->eof()) + { // update + int artId = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId); + m_pDS->exec(sql); + } + else + { // insert + m_pDS->close(); + sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) " + "VALUES (%d, '%s', '%s', '%s')", + mediaId, mediaType.c_str(), artType.c_str(), url.c_str()); + m_pDS->exec(sql); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType, + artType, url); + } +} + +bool CMusicDatabase::GetArtForItem( + int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader>& art) +{ + std::string strSQL; + try + { + if (!(songId > 0 || albumId > 0 || artistId > 0)) + return false; + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + Filter filter; + if (songId > 0) + filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong)); + if (albumId > 0) + filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum), + false); + if (artistId > 0) + filter.AppendWhere( + PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false); + + strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder " + "FROM art"; + if (!BuildSQL(strSQL, filter, strSQL)) + return false; + + if (!(artistId > 0)) + { + // Artist ID unknown, so lookup album artist for albums and songs + std::string strSQL2; + if (albumId > 0) + { + //Album ID known, so use it to look up album artist(s) + strSQL2 = PrepareSQL( + "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, " + "url, album_artist.iOrder as iorder FROM art " + "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' " + "WHERE album_artist.idAlbum = %i ", + MediaTypeArtist, albumId); + if (bPrimaryArtist) + strSQL2 += "AND album_artist.iOrder = 0"; + + strSQL = strSQL + " UNION " + strSQL2; + } + if (songId > 0) + { + if (albumId < 0) + { + //Album ID unknown, so get from song to look up album artist(s) + strSQL2 = PrepareSQL( + "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, " + "url, album_artist.iOrder as iorder FROM art " + "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' " + "JOIN song ON song.idAlbum = album_artist.idAlbum " + "WHERE song.idSong = %i ", + MediaTypeArtist, songId); + if (bPrimaryArtist) + strSQL2 += "AND album_artist.iOrder = 0"; + + strSQL = strSQL + " UNION " + strSQL2; + } + + // Artist ID unknown, so lookup artist for songs (could be different from album artist) + strSQL2 = PrepareSQL( + "SELECT art_id, media_id, media_type, type, 'artist' as prefix, " + "url, song_artist.iOrder as iorder FROM art " + "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' " + "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ", + MediaTypeArtist, songId, ROLE_ARTIST); + if (bPrimaryArtist) + strSQL2 += "AND song_artist.iOrder = 0"; + + strSQL = strSQL + " UNION " + strSQL2; + } + } + if (songId > 0 && albumId < 0) + { + //Album ID unknown, so get from song to look up album art + std::string strSQL2; + strSQL2 = PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, " + "url, 0 as iorder FROM art " + "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' " + "WHERE song.idSong = %i ", + MediaTypeAlbum, songId); + strSQL = strSQL + " UNION " + strSQL2; + } + + m_pDS2->query(strSQL); + while (!m_pDS2->eof()) + { + ArtForThumbLoader artitem; + artitem.artType = m_pDS2->fv("type").get_asString(); + artitem.mediaType = m_pDS2->fv("media_type").get_asString(); + artitem.prefix = m_pDS2->fv("prefix").get_asString(); + artitem.url = m_pDS2->fv("url").get_asString(); + int iOrder = m_pDS2->fv("iorder").get_asInt(); + // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2" + if (iOrder > 0) + artitem.prefix += m_pDS2->fv("iorder").get_asString(); + + art.emplace_back(artitem); + m_pDS2->next(); + } + m_pDS2->close(); + return !art.empty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strSQL); + } + return false; +} + +bool CMusicDatabase::GetArtForItem(int mediaId, + const std::string& mediaType, + std::map<std::string, std::string>& art) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'", + mediaId, mediaType.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString())); + m_pDS2->next(); + } + m_pDS2->close(); + return !art.empty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId); + } + return false; +} + +std::string CMusicDatabase::GetArtForItem(int mediaId, + const std::string& mediaType, + const std::string& artType) +{ + std::string query = PrepareSQL("SELECT url FROM art " + "WHERE media_id=%i AND media_type='%s' AND type='%s'", + mediaId, mediaType.c_str(), artType.c_str()); + return GetSingleValue(query, m_pDS2); +} + +bool CMusicDatabase::RemoveArtForItem(int mediaId, + const MediaType& mediaType, + const std::string& artType) +{ + return ExecuteQuery(PrepareSQL("DELETE FROM art " + "WHERE media_id=%i AND media_type='%s' AND type='%s'", + mediaId, mediaType.c_str(), artType.c_str())); +} + +bool CMusicDatabase::RemoveArtForItem(int mediaId, + const MediaType& mediaType, + const std::set<std::string>& artTypes) +{ + bool result = true; + for (const auto& i : artTypes) + result &= RemoveArtForItem(mediaId, mediaType, i); + + return result; +} + +bool CMusicDatabase::GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL = + PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str()); + + if (!m_pDS->query(strSQL)) + return false; + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return false; + } + + while (!m_pDS->eof()) + { + artTypes.emplace_back(m_pDS->fv(0).get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType); + } + return false; +} + +std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId, + const MediaType& mediaType) +{ + CScraperUrl thumbURL; + if (mediaType == MediaTypeArtist) + { + CArtist artist; + if (GetArtist(mediaId, artist)) + thumbURL = artist.thumbURL; + } + else if (mediaType == MediaTypeAlbum) + { + CAlbum album; + if (GetAlbum(mediaId, album)) + thumbURL = album.thumbURL; + } + + std::vector<std::string> result; + for (const auto& urlEntry : thumbURL.GetUrls()) + { + std::string artType = urlEntry.m_aspect; + if (artType.empty()) + artType = "thumb"; + if (std::find(result.begin(), result.end(), artType) == result.end()) + result.push_back(artType); + } + return result; +} + +std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem( + int mediaId, const MediaType& mediaType, const std::string& artType) +{ + CScraperUrl thumbURL; + if (mediaType == MediaTypeArtist) + { + CArtist artist; + if (GetArtist(mediaId, artist)) + thumbURL = artist.thumbURL; + } + else if (mediaType == MediaTypeAlbum) + { + CAlbum album; + if (GetAlbum(mediaId, album)) + thumbURL = album.thumbURL; + } + + std::vector<CScraperUrl::SUrlEntry> result; + for (auto urlEntry : thumbURL.GetUrls()) + { + if (urlEntry.m_aspect.empty()) + urlEntry.m_aspect = "thumb"; + if (artType.empty() || urlEntry.m_aspect == artType) + result.push_back(urlEntry); + } + return result; +} + +int CMusicDatabase::GetOrderFilter(const std::string& type, + const SortDescription& sorting, + Filter& filter) +{ + // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort + int iFieldsAdded = 0; + filter.fields.clear(); // remove "*" + std::vector<std::string> orderfields; + std::string DESC; + + if (sorting.sortOrder == SortOrderDescending) + DESC = " DESC"; + + if (sorting.sortBy == SortByRandom) + orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL + else + { + FieldList fields; + SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields); + for (const auto& it : fields) + { + std::string strField; + if (it == FieldYear) + strField = "iYear"; + else + strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect); + if (!strField.empty()) + orderfields.emplace_back(strField); + } + } + + // Convert field names into order by statement elements + for (auto& name : orderfields) + { + //Add field for adjusted name sorting using sort name and ignoring articles + std::string sortSQL; + if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist")) + { + if (StringUtils::EndsWith(name, "strArtists")) + sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort"); + else + sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName"); + if (!sortSQL.empty()) + { + name = "artistsortname"; + filter.AppendField(sortSQL); // Add artistsortname as scalar query field + iFieldsAdded++; + } + // Natural number case-insensitive sort + filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder)); + } + else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle")) + { + sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, ""); + if (!sortSQL.empty()) + { + name = "titlesortname"; + filter.AppendField(sortSQL); // Add sortname as scalar query field + iFieldsAdded++; + } + // Natural number case-insensitive sort + filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder)); + } + else if (StringUtils::EndsWith(name, "strGenres")) + // Natural number case-insensitive sort + filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder)); + else + filter.AppendOrder(name + DESC); + } + return iFieldsAdded; +} + +bool CMusicDatabase::GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting) +{ + if (!musicUrl.IsValid()) + return false; + + std::string type = musicUrl.GetType(); + const CUrlOptions::UrlOptions& options = musicUrl.GetOptions(); + + // Check for playlist rules first, they may contain role criteria + bool hasRoleRules = false; + + auto option = options.find("xsp"); + if (option != options.end()) + { + CSmartPlaylist xsp; + if (!xsp.LoadFromJson(option->second.asString())) + return false; + + std::set<std::string> playlists; + std::string xspWhere; + xspWhere = xsp.GetWhereClause(*this, playlists); + hasRoleRules = xsp.GetType() == "artists" && + xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos; + + // Check if the filter playlist matches the item type + // Allow for grouping name like "originalyears" and type "years" + if (xsp.GetType() == type || + (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed())) + { + filter.AppendWhere(xspWhere); + + if (xsp.GetLimit() > 0) + sorting.limitEnd = xsp.GetLimit(); + if (xsp.GetOrder() != SortByNone) + sorting.sortBy = xsp.GetOrder(); + sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sorting.sortAttributes = SortAttributeIgnoreArticle; + } + } + + //Process role options, common to artist and album type filtering + int idRole = 1; // Default restrict song_artist to "artists" only, no other roles. + option = options.find("roleid"); + if (option != options.end()) + idRole = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("role"); + if (option != options.end()) + { + if (option->second.asString() == "all" || option->second.asString() == "%") + idRole = -1000; //All roles + else + idRole = GetRoleByName(option->second.asString()); + } + } + if (hasRoleRules) + { + // Get Role from role rule(s) here. + // But that requires much change, so for now get all roles as better than none + idRole = -1000; //All roles + } + + std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role + if (idRole > 0) + strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole); + + int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1; + int idDisc = -1; + int idSource = -1; + bool albumArtistsOnly = false; + bool useOriginalYear = false; + std::string artistname; + + // Process useoriginalyear option, setting overridden by option + useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE); + option = options.find("useoriginalyear"); + if (option != options.end()) + useOriginalYear = option->second.asBoolean(); + + // Process albumartistsonly option + option = options.find("albumartistsonly"); + if (option != options.end()) + albumArtistsOnly = option->second.asBoolean(); + + // Process genre option + option = options.find("genreid"); + if (option != options.end()) + idGenre = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("genre"); + if (option != options.end()) + idGenre = GetGenreByName(option->second.asString()); + } + + // Process source option + option = options.find("sourceid"); + if (option != options.end()) + idSource = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("source"); + if (option != options.end()) + idSource = GetSourceByName(option->second.asString()); + } + + // Process album option + option = options.find("albumid"); + if (option != options.end()) + idAlbum = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("album"); + if (option != options.end()) + idAlbum = GetAlbumByName(option->second.asString()); + } + + // Process artist option + option = options.find("artistid"); + if (option != options.end()) + idArtist = static_cast<int>(option->second.asInteger()); + else + { + option = options.find("artist"); + if (option != options.end()) + { + idArtist = GetArtistByName(option->second.asString()); + if (idArtist == -1) + { // not found with that name, or more than one found as artist name is not unique + artistname = option->second.asString(); + } + } + } + + // Process song option + option = options.find("songid"); + if (option != options.end()) + idSong = static_cast<int>(option->second.asInteger()); + + if (type == "artists") + { + if (!hasRoleRules) + { // Not an "artists" smart playlist with roles rules, so get filter from options + if (idArtist > 0) + filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist)); + else if (idAlbum > 0) + filter.AppendWhere( + PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist " + "WHERE album_artist.idAlbum = %i)", + idAlbum)); + else if (idSong > 0) + { + filter.AppendWhere( + PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist " + "WHERE song_artist.idSong = %i %s)", + idSong, strRoleSQL.c_str())); + } + else + { /* + Process idRole, idGenre, idSource and albumArtistsOnly options + + For artists these rules are combined because they apply via album and song + and so we need to ensure all criteria are met via the same album or song. + 1) Some artists may be only album artists, so for all artists (with linked + albums or songs) we need to check both album_artist and song_artist tables. + 2) Role is determined from song_artist table, so even if looking for album artists + only we find those that also have a specific role e.g. which album artist is a + composer of songs in that album, from entries in the song_artist table. + a) Role < -1 is used to indicate that all roles are wanted. + b) When not album artists only and a specific role wanted then only the song_artist + table is checked. + c) When album artists only and role = 1 (an "artist") then only the album_artist + table is checked. + */ + std::string albumArtistSQL, songArtistSQL; + ExistsSubQuery albumArtistSub("album_artist", + "album_artist.idArtist = artistview.idArtist"); + // Prepare album artist subquery SQL + if (idSource > 0) + { + if (idRole == 1 && idGenre < 0) + { + albumArtistSub.AppendJoin( + "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum"); + albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource)); + } + else + { + albumArtistSub.AppendWhere( + PrepareSQL("EXISTS(SELECT 1 FROM album_source " + "WHERE album_source.idSource = %i " + "AND album_source.idAlbum = album_artist.idAlbum)", + idSource)); + } + } + if (idRole <= 1 && idGenre > 0) + { // Check genre of songs of album using nested subquery + std::string strGenre = + PrepareSQL("EXISTS(SELECT 1 FROM song " + "JOIN song_genre ON song_genre.idSong = song.idSong " + "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)", + idGenre); + albumArtistSub.AppendWhere(strGenre); + } + + // Prepare song artist subquery SQL + ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist"); + if (idRole > 0) + songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole)); + if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1) + { + songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song " + "JOIN song_genre ON song_genre.idSong = song.idSong " + "WHERE song.idSong = song_artist.idSong " + "AND song_genre.idGenre = %i " + "AND EXISTS(SELECT 1 FROM album_source " + "WHERE album_source.idSource = %i " + "AND album_source.idAlbum = song.idAlbum))", + idGenre, idSource)); + } + else + { + if (idGenre > 0) + { + songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong"); + songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre)); + } + if (idSource > 0 && !albumArtistsOnly) + { + songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong"); + songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum"); + songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource)); + } + if (idRole > 1 && albumArtistsOnly) + { // Album artists only with role, check AND in album_artist for album of song + // using nested subquery correlated with album_artist + songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong"); + songArtistSub.param = "song_artist.idArtist = album_artist.idArtist"; + songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum"); + } + } + + // Build filter clause from subqueries + if (idRole > 1 && albumArtistsOnly) + { // Album artists only with role, check AND in album_artist for album of song + // using nested subquery correlated with album_artist + songArtistSub.BuildSQL(songArtistSQL); + albumArtistSub.AppendWhere(songArtistSQL); + albumArtistSub.BuildSQL(albumArtistSQL); + filter.AppendWhere(albumArtistSQL); + } + else + { + songArtistSub.BuildSQL(songArtistSQL); + albumArtistSub.BuildSQL(albumArtistSQL); + if (idRole < 0 || (idRole == 1 && !albumArtistsOnly)) + { // Artist contributing to songs, any role, check OR album artist too + // as artists can be just album artists but not song artists + filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL); + } + else if (idRole > 1) + { + // Artist contributes that role (not albmartistsonly as already handled) + filter.AppendWhere(songArtistSQL); + } + else // idRole = 1 and albumArtistsOnly + { // Only look at album artists, not albums where artist features on songs + filter.AppendWhere(albumArtistSQL); + } + } + } + } + // remove the null string + filter.AppendWhere("artistview.strArtist != ''"); + } + else if (type == "albums") + { + option = options.find("year"); + if (option != options.end()) + { + if (!useOriginalYear) + filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + else + filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + } + option = options.find("compilation"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0)); + + option = options.find("boxset"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0)); + + if (idSource > 0) + filter.AppendWhere(PrepareSQL( + "EXISTS(SELECT 1 FROM album_source " + "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", + idSource)); + + // Process artist, role and genre options together as song subquery to filter those + // albums that have songs with both that artist and genre + std::string albumArtistSQL, songArtistSQL, genreSQL; + ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum"); + genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong"); + genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre)); + ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum"); + ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum"); + songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong"); + + if (idArtist > 0) + { + songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist)); + albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist)); + } + else if (!artistname.empty()) + { // Artist name is not unique, so could get albums or songs from more than one. + songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist"); + songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str())); + + albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist"); + albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str())); + } + if (idRole > 0) + songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole)); + if (idGenre > 0) + { + songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong"); + songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre)); + } + + if (idArtist > 0 || !artistname.empty()) + { + if (idRole <= 1 && idGenre > 0) + { // Check genre of songs of album using nested subquery + genreSub.BuildSQL(genreSQL); + albumArtistSub.AppendWhere(genreSQL); + } + if (idRole > 1 && albumArtistsOnly) + { // Album artists only with role, check AND in album_artist for same song + // using nested subquery correlated with album_artist + songArtistSub.param = "song.idAlbum = album_artist.idAlbum"; + songArtistSub.BuildSQL(songArtistSQL); + albumArtistSub.AppendWhere(songArtistSQL); + albumArtistSub.BuildSQL(albumArtistSQL); + filter.AppendWhere(albumArtistSQL); + } + else + { + songArtistSub.BuildSQL(songArtistSQL); + albumArtistSub.BuildSQL(albumArtistSQL); + if (idRole < 0 || (idRole == 1 && !albumArtistsOnly)) + { // Artist contributing to songs, any role, check OR album artist too + // as artists can be just album artists but not song artists + filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL); + } + else if (idRole > 1) + { // Albums with songs where artist contributes that role (not albmartistsonly as already handled) + filter.AppendWhere(songArtistSQL); + } + else // idRole = 1 and albumArtistsOnly + { // Only look at album artists, not albums where artist features on songs + // This may want to be a separate option so you can choose to see all the albums where that artist + // appears on one or more songs without having to list all song artists in the artists node. + filter.AppendWhere(albumArtistSQL); + } + } + } + else + { // No artist given + if (idGenre > 0) + { // Have genre option but not artist + genreSub.param = "song.idAlbum = albumview.idAlbum"; + genreSub.BuildSQL(genreSQL); + filter.AppendWhere(genreSQL); + } + // Exclude any single albums (aka empty tagged albums) + // This causes "albums" media filter artist selection to only offer album artists + option = options.find("show_singles"); + if (option == options.end() || !option->second.asBoolean()) + filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'", + CAlbum::ReleaseTypeToString(CAlbum::Album).c_str())); + } + } + else if (type == "discs") + { + if (idAlbum > 0) + filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum)); + else + { + option = options.find("year"); + if (option != options.end()) + { + if (!useOriginalYear) + filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + else + filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + } + + option = options.find("compilation"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0)); + + option = options.find("boxset"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0)); + + if (idSource > 0) + filter.AppendWhere(PrepareSQL( + "EXISTS(SELECT 1 FROM album_source " + "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", + idSource)); + } + option = options.find("discid"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger())); + + option = options.find("disctitle"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str())); + + if (idGenre > 0) + filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = " + "song.idSong AND song_genre.idGenre = %i)", + idGenre)); + + std::string songArtistClause, albumArtistClause; + if (idArtist > 0) + { + songArtistClause = + PrepareSQL("EXISTS (SELECT 1 FROM song_artist " + "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)", + idArtist, strRoleSQL.c_str()); + albumArtistClause = + PrepareSQL("EXISTS (SELECT 1 FROM album_artist " + "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)", + idArtist); + } + else if (!artistname.empty()) + { // Artist name is not unique, so could get songs from more than one. + songArtistClause = PrepareSQL( + "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist " + "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)", + artistname.c_str(), strRoleSQL.c_str()); + albumArtistClause = + PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = " + "album_artist.idArtist " + "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')", + artistname.c_str()); + } + + // Process artist name or id option + if (!songArtistClause.empty()) + { + if (idRole < 0) // Artist contributes to songs, any roles OR is album artist + filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")"); + else if (idRole > 1) + { + if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song + filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")"); + else // songs where artist contributes that role. + filter.AppendWhere(songArtistClause); + } + else + { + if (albumArtistsOnly) // Only look at album artists, not where artist features on songs + filter.AppendWhere(albumArtistClause); + else // Artist is song artist or album artist + filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")"); + } + } + } + else if (type == "songs" || type == "singles") + { + option = options.find("singles"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL( + "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')", + option->second.asBoolean() ? "" : "NOT ", + CAlbum::ReleaseTypeToString(CAlbum::Single).c_str())); + + // When have idAlbum skip year, compilation, boxset criteria as already applied via album + if (idAlbum < 0) + { + option = options.find("year"); + if (option != options.end()) + { + if (!useOriginalYear) + filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + else + filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'", + option->second.asString().c_str())); + } + option = options.find("compilation"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0)); + + option = options.find("boxset"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = " + "songview.idAlbum AND bBoxedSet = %i)", + option->second.asBoolean() ? 1 : 0)); + } + + option = options.find("discid"); + if (option != options.end()) + idDisc = static_cast<int>(option->second.asInteger()); + + option = options.find("disctitle"); + if (option != options.end()) + filter.AppendWhere( + PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str())); + + if (idSong > 0) + filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong)); + + if (idAlbum > 0) + filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum)); + + if (idDisc > 0) + filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc)); + + if (idGenre > 0) + filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre " + "WHERE song_genre.idGenre = %i)", + idGenre)); + + if (idSource > 0) + filter.AppendWhere(PrepareSQL( + "EXISTS(SELECT 1 FROM album_source " + "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)", + idSource)); + + std::string songArtistClause, albumArtistClause; + if (idArtist > 0) + { + songArtistClause = + PrepareSQL("EXISTS (SELECT 1 FROM song_artist " + "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)", + idArtist, strRoleSQL.c_str()); + albumArtistClause = PrepareSQL( + "EXISTS (SELECT 1 FROM album_artist " + "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)", + idArtist); + } + else if (!artistname.empty()) + { // Artist name is not unique, so could get songs from more than one. + songArtistClause = PrepareSQL( + "EXISTS (SELECT 1 FROM song_artist " + "JOIN artist ON artist.idArtist = song_artist.idArtist " + "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)", + artistname.c_str(), strRoleSQL.c_str()); + albumArtistClause = PrepareSQL( + "EXISTS (SELECT 1 FROM album_artist " + "JOIN artist ON artist.idArtist = album_artist.idArtist " + "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')", + artistname.c_str()); + } + + // Process artist name or id option + if (!songArtistClause.empty()) + { + if (idRole < 0) // Artist contributes to songs, any roles OR is album artist + filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")"); + else if (idRole > 1) + { + if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song + filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")"); + else // songs where artist contributes that role. + filter.AppendWhere(songArtistClause); + } + else + { + if (albumArtistsOnly) // Only look at album artists, not where artist features on songs + filter.AppendWhere(albumArtistClause); + else // Artist is song artist or album artist + filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")"); + } + } + } + + option = options.find("filter"); + if (option != options.end()) + { + CSmartPlaylist xspFilter; + if (!xspFilter.LoadFromJson(option->second.asString())) + return false; + + // check if the filter playlist matches the item type + if (xspFilter.GetType() == type) + { + std::set<std::string> playlists; + filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists)); + } + // remove the filter if it doesn't match the item type + else + musicUrl.RemoveOption("filter"); + } + + return true; +} + +std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath) +{ + if (strFileNameAndPath.empty()) + return std::string(); + + CDateTime dateMedia; + int code; + code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded; + // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid + if (code == 1) + dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath); + //2 using the newer datetime of the file's mtime and ctime + else if (code == 2) + dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath); + //3 using the older datetime of the file's mtime and ctime + else if (code == 3) + dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath); + //0 using the current datetime if none of the above matches or one returns an invalid datetime + if (!dateMedia.IsValid()) + dateMedia = CDateTime::GetCurrentDateTime(); + + return dateMedia.GetAsDBDateTime(); +} + +bool CMusicDatabase::AddAudioBook(const CFileItem& item) +{ + auto const& artists = item.GetMusicInfoTag()->GetArtist(); + std::string strSQL = PrepareSQL( + "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) " + "VALUES (NULL,'%s','%s',%i,'%s','%s')", + item.GetMusicInfoTag()->GetAlbum().c_str(), artists.empty() ? "" : artists[0].c_str(), 0, + item.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()); + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark) +{ + std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook " + "WHERE file='%s'", + item.GetDynPath().c_str()); + if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0) + { + if (!AddAudioBook(item)) + return false; + } + + strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i " + "WHERE file='%s'", + bookmark, item.GetDynPath().c_str()); + + return ExecuteQuery(strSQL); +} + +bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark) +{ + std::string strSQL = + PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item.GetDynPath().c_str()); + if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0) + return false; + + bookmark = m_pDS->fv(0).get_asInt(); + return true; +} diff --git a/xbmc/music/MusicDatabase.h b/xbmc/music/MusicDatabase.h new file mode 100644 index 0000000..8544eb2 --- /dev/null +++ b/xbmc/music/MusicDatabase.h @@ -0,0 +1,1157 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +/*! + \file MusicDatabase.h +\brief +*/ + +#include "Album.h" +#include "MediaSource.h" +#include "addons/Scraper.h" +#include "dbwrappers/Database.h" +#include "settings/LibExportSettings.h" +#include "utils/SortUtils.h" + +#include <utility> +#include <vector> + +class CArtist; +class CFileItem; +class CMusicDbUrl; + +namespace dbiplus +{ +class field_value; +typedef std::vector<field_value> sql_record; +} // namespace dbiplus + +#include <set> +#include <string> + +// return codes of Cleaning up the Database +// numbers are strings from strings.po +#define ERROR_OK 317 +#define ERROR_CANCEL 0 +#define ERROR_DATABASE 315 +#define ERROR_REORG_SONGS 319 +#define ERROR_REORG_ARTIST 321 +#define ERROR_REORG_OTHER 323 +#define ERROR_REORG_PATH 325 +#define ERROR_REORG_ALBUM 327 +#define ERROR_WRITING_CHANGES 329 +#define ERROR_COMPRESSING 332 + +#define NUM_SONGS_BEFORE_COMMIT 500 + +/*! + \ingroup music + \brief A set of std::string objects, used for CMusicDatabase + \sa ISETPATHS, CMusicDatabase + */ +typedef std::set<std::string> SETPATHS; + +/*! + \ingroup music + \brief The SETPATHS iterator + \sa SETPATHS, CMusicDatabase + */ +typedef std::set<std::string>::iterator ISETPATHS; + +/*! +\ingroup music +\brief A structure used for fetching music art data +\sa CMusicDatabase::GetArtForItem() +*/ + +typedef struct +{ + std::string mediaType; + std::string artType; + std::string prefix; + std::string url; +} ArtForThumbLoader; + +class CGUIDialogProgress; +class CFileItemList; + +/*! + \ingroup music + \brief Class to store and read tag information + + CMusicDatabase can be used to read and store + tag information for faster access. It is based on + sqlite (http://www.sqlite.org). + + Here is the database layout: + \image html musicdatabase.png + + \sa CAlbum, CSong, VECSONGS, CMapSong, VECARTISTS, VECALBUMS, VECGENRES + */ +class CMusicDatabase : public CDatabase +{ + friend class DatabaseUtils; + friend class TestDatabaseUtilsHelper; + +public: + CMusicDatabase(void); + ~CMusicDatabase(void) override; + + bool Open() override; + bool CommitTransaction() override; + void EmptyCache(); + void Clean(); + int Cleanup(CGUIDialogProgress* progressDialog = nullptr); + bool LookupCDDBInfo(bool bRequery = false); + void DeleteCDDBInfo(); + + ///////////////////////////////////////////////// + // Song CRUD + ///////////////////////////////////////////////// + /*! \brief Add a song to the database + \param idSong [in] the original database ID of the song to reuse (-1 when new) + \param dtDateNew [in] the datetime the original ID was new + \param idAlbum [in] the database ID of the album for the song + \param strTitle [in] the title of the song (required to be non-empty) + \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song + \param strPathAndFileName [in] the path and filename to the song + \param strComment [in] the ids of the added songs + \param strMood [in] the mood of the added song + \param strThumb [in] the ids of the added songs + \param artistDisp [in] the assembled artist name(s) display string + \param artistSort [in] the artist name(s) sort string + \param genres [in] a vector of genres to which this song belongs + \param iTrack [in] the track number and disc number of the song + \param iDuration [in] the duration of the song + \param strReleaseDate [in] the release date of the song ISO8601 format + \param strOrigReleaseDate [in] the original release date of the song ISO8601 format + \param strDiscSubtitle [in] subtitle of a disc + \param iTimesPlayed [in] the number of times the song has been played + \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue) + \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue) + \param dtLastPlayed [in] the time the song was last played + \param rating [in] a rating for the song + \param userrating [in] a userrating (my rating) for the song + \param votes [in] a vote counter for the song rating + \param replayGain [in] album and track replaygain and peak values + \return the id of the song + */ + int AddSong(const int idSong, + const CDateTime& dtDateNew, + const int idAlbum, + const std::string& strTitle, + const std::string& strMusicBrainzTrackID, + const std::string& strPathAndFileName, + const std::string& strComment, + const std::string& strMood, + const std::string& strThumb, + const std::string& artistDisp, + const std::string& artistSort, + const std::vector<std::string>& genres, + int iTrack, + int iDuration, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + std::string& strDiscSubtitle, + const int iTimesPlayed, + int iStartOffset, + int iEndOffset, + const CDateTime& dtLastPlayed, + float rating, + int userrating, + int votes, + int iBPM, + int iBitRate, + int iSampleRate, + int iChannels, + const ReplayGain& replayGain); + bool GetSong(int idSong, CSong& song); + + /*! \brief Update a song and all its nested entities (genres, artists, contributors) + \param song [in/out] the song to update, artist ids are returned in artist credits + \param bArtists to update artist credits and contributors, default is true + \param bArtists to check and log if artist links have changed, default is true + \return true if successful + */ + bool UpdateSong(CSong& song, bool bArtists = true, bool bArtistLinks = true); + + /*! \brief Update a song in the database + \param idSong [in] the database ID of the song to update + \param strTitle [in] the title of the song (required to be non-empty) + \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song + \param strPathAndFileName [in] the path and filename to the song + \param strComment [in] the ids of the added songs + \param strMood [in] the mood of the added song + \param strThumb [in] the ids of the added songs + \param artistDisp [in] the artist name(s) display string + \param artistSort [in] the artist name(s) sort string + \param genres [in] a vector of genres to which this song belongs + \param iTrack [in] the track number and disc number of the song + \param iDuration [in] the duration of the song + \param strReleaseDate [in] the release date of the song ISO8601 format + \param strOrigReleaseDate [in] the original release date of the song ISO8601 format + \param strDiscSubtitle [in] subtitle of a disc + \param iTimesPlayed [in] the number of times the song has been played + \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue) + \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue) + \param dtLastPlayed [in] the time the song was last played + \param rating [in] a rating for the song + \param userrating [in] a userrating (my rating) for the song + \param votes [in] a vote counter for the song rating + \param replayGain [in] album and track replaygain and peak values + \param iBPM [in] the beats per minute of a song + \param iBitRate [in] the bitrate of the song file + \param iSampleRate [in] the sample rate of the song file + \param iChannels [in] the number of audio channels in the song file + \return the id of the song + */ + int UpdateSong(int idSong, + const std::string& strTitle, + const std::string& strMusicBrainzTrackID, + const std::string& strPathAndFileName, + const std::string& strComment, + const std::string& strMood, + const std::string& strThumb, + const std::string& artistDisp, + const std::string& artistSort, + const std::vector<std::string>& genres, + int iTrack, + int iDuration, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + const std::string& strDiscSubtitle, + int iTimesPlayed, + int iStartOffset, + int iEndOffset, + const CDateTime& dtLastPlayed, + float rating, + int userrating, + int votes, + const ReplayGain& replayGain, + int iBPM, + int iBitRate, + int iSampleRate, + int iChannels); + + //// Misc Song + bool GetSongByFileName(const std::string& strFileName, CSong& song, int64_t startOffset = 0); + bool GetSongsByPath(const std::string& strPath, MAPSONGS& songmap, bool bAppendToMap = false); + bool Search(const std::string& search, CFileItemList& items); + bool RemoveSongsFromPath(const std::string& path, MAPSONGS& songmap, bool exact = true); + void CheckArtistLinksChanged(); + bool SetSongUserrating(const std::string& filePath, int userrating); + bool SetSongUserrating(int idSong, int userrating); + bool SetSongVotes(const std::string& filePath, int votes); + int GetSongByArtistAndAlbumAndTitle(const std::string& strArtist, + const std::string& strAlbum, + const std::string& strTitle); + + ///////////////////////////////////////////////// + // Album + ///////////////////////////////////////////////// + /*! \brief Add an album and all its songs to the database + \param album the album to add + \param idSource the music source id + \return the id of the album + */ + bool AddAlbum(CAlbum& album, int idSource); + + /*! \brief Update an album and all its nested entities (artists, songs etc) + \param album the album to update + \return true or false + */ + bool UpdateAlbum(CAlbum& album); + + /*! \brief Add an album to the database + \param strAlbum the album title + \param strMusicBrainzAlbumID the Musicbrainz Id + \param strArtist the album artist name(s) display string + \param strArtistSort the album artist name(s) sort string + \param strGenre the album genre(s) + \param strReleaseDate [in] the release date of the album ISO8601 format + \param strOrigReleaseDate [in] the original release date of the album ISO8601 format + \param bBoxedSet if the album is a boxset + \param strRecordLabel the recording label + \param strType album type (Musicbrainz release type e.g. "Broadcast, Soundtrack, live"), + \param strReleaseStatus (see https://musicbrainz.org/doc/Release#Status) + \param bCompilation if the album is a compilation + \param releaseType "album" or "single" + \return the id of the album + */ + int AddAlbum(const std::string& strAlbum, + const std::string& strMusicBrainzAlbumID, + const std::string& strReleaseGroupMBID, + const std::string& strArtist, + const std::string& strArtistSort, + const std::string& strGenre, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + bool bBoxedSet, + const std::string& strRecordLabel, + const std::string& strType, + const std::string& strReleaseStatus, + bool bCompilation, + CAlbum::ReleaseType releaseType); + + /*! \brief retrieve an album, optionally with all songs. + \param idAlbum the database id of the album. + \param album [out] the album to fill. + \param getSongs whether or not to retrieve songs, defaults to true. + \return true if the album is retrieved, false otherwise. + */ + bool GetAlbum(int idAlbum, CAlbum& album, bool getSongs = true); + int UpdateAlbum(int idAlbum, + const std::string& strAlbum, + const std::string& strMusicBrainzAlbumID, + const std::string& strReleaseGroupMBID, + const std::string& strArtist, + const std::string& strArtistSort, + const std::string& strGenre, + const std::string& strMoods, + const std::string& strStyles, + const std::string& strThemes, + const std::string& strReview, + const std::string& strImage, + const std::string& strLabel, + const std::string& strType, + const std::string& strReleaseStatus, + float fRating, + int iUserrating, + int iVotes, + const std::string& strReleaseDate, + const std::string& strOrigReleaseDate, + bool bBoxedSet, + bool bCompilation, + CAlbum::ReleaseType releaseType, + bool bScrapedMBID); + bool ClearAlbumLastScrapedTime(int idAlbum); + bool HasAlbumBeenScraped(int idAlbum); + + ///////////////////////////////////////////////// + // Audiobook + ///////////////////////////////////////////////// + bool AddAudioBook(const CFileItem& item); + bool SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark); + bool GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark); + + /*! \brief Checks if the given path is inside a folder that has already been scanned into the library + \param path the path we want to check + */ + bool InsideScannedPath(const std::string& path); + + //// Misc Album + int GetAlbumIdByPath(const std::string& path); + bool GetAlbumFromSong(int idSong, CAlbum& album); + int GetAlbumByName(const std::string& strAlbum, const std::string& strArtist = ""); + int GetAlbumByName(const std::string& strAlbum, const std::vector<std::string>& artist); + bool GetMatchingMusicVideoAlbum(const std::string& strAlbum, + const std::string& strArtist, + int& idAlbum, + std::string& strReview); + bool SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items); + int GetAlbumByMatch(const CAlbum& album); + std::string GetAlbumById(int id); + std::string GetAlbumDiscTitle(int idAlbum, int idDisc); + bool SetAlbumUserrating(const int idAlbum, int userrating); + int GetAlbumDiscsCount(int idAlbum); + + ///////////////////////////////////////////////// + // Artist CRUD + ///////////////////////////////////////////////// + bool UpdateArtist(const CArtist& artist); + + int AddArtist(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + const std::string& strSortName, + bool bScrapedMBID = false); + int AddArtist(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + bool bScrapedMBID = false); + bool GetArtist(int idArtist, CArtist& artist, bool fetchAll = false); + bool GetArtistExists(int idArtist); + int GetLastArtist(); + int GetArtistFromMBID(const std::string& strMusicBrainzArtistID, std::string& artistname); + int UpdateArtist(int idArtist, + const std::string& strArtist, + const std::string& strSortName, + const std::string& strMusicBrainzArtistID, + bool bScrapedMBID, + const std::string& strType, + const std::string& strGender, + const std::string& strDisambiguation, + const std::string& strBorn, + const std::string& strFormed, + const std::string& strGenres, + const std::string& strMoods, + const std::string& strStyles, + const std::string& strInstruments, + const std::string& strBiography, + const std::string& strDied, + const std::string& strDisbanded, + const std::string& strYearsActive, + const std::string& strImage); + bool UpdateArtistScrapedMBID(int idArtist, const std::string& strMusicBrainzArtistID); + bool GetTranslateBlankArtist() { return m_translateBlankArtist; } + void SetTranslateBlankArtist(bool translate) { m_translateBlankArtist = translate; } + bool HasArtistBeenScraped(int idArtist); + bool ClearArtistLastScrapedTime(int idArtist); + int AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum); + bool DeleteArtistDiscography(int idArtist); + bool GetArtistDiscography(int idArtist, CFileItemList& items); + + std::string GetArtistById(int id); + int GetArtistByName(const std::string& strArtist); + int GetArtistByMatch(const CArtist& artist); + bool GetArtistFromSong(int idSong, CArtist& artist); + bool IsSongArtist(int idSong, int idArtist); + bool IsSongAlbumArtist(int idSong, int idArtist); + std::string GetRoleById(int id); + + /*! \brief Propagate artist sort name into the concatenated artist sort name strings + held for songs and albums + \param int idArtist to propagate sort name for, -1 means all artists + */ + bool UpdateArtistSortNames(int idArtist = -1); + + ///////////////////////////////////////////////// + // Paths + ///////////////////////////////////////////////// + int AddPath(const std::string& strPath); + + bool GetPaths(std::set<std::string>& paths); + bool SetPathHash(const std::string& path, const std::string& hash); + bool GetPathHash(const std::string& path, std::string& hash); + bool GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths); + bool GetAlbumPath(int idAlbum, std::string& basePath); + int GetDiscnumberForPathID(int idPath); + bool GetOldArtistPath(int idArtist, std::string& path); + bool GetArtistPath(const CArtist& artist, std::string& path); + bool GetAlbumFolder(const CAlbum& album, const std::string& strAlbumPath, std::string& strFolder); + bool GetArtistFolderName(const CArtist& artist, std::string& strFolder); + bool GetArtistFolderName(const std::string& strArtist, + const std::string& strMusicBrainzArtistID, + std::string& strFolder); + + ///////////////////////////////////////////////// + // Sources + ///////////////////////////////////////////////// + bool UpdateSources(); + int AddSource(const std::string& strName, + const std::string& strMultipath, + const std::vector<std::string>& vecPaths, + int id = -1); + int UpdateSource(const std::string& strOldName, + const std::string& strName, + const std::string& strMultipath, + const std::vector<std::string>& vecPaths); + bool RemoveSource(const std::string& strName); + int GetSourceFromPath(const std::string& strPath); + bool AddAlbumSource(int idAlbum, int idSource); + bool AddAlbumSources(int idAlbum, const std::string& strPath); + bool DeleteAlbumSources(int idAlbum); + bool GetSources(CFileItemList& items); + + bool GetSourcesByArtist(int idArtist, CFileItem* item); + bool GetSourcesByAlbum(int idAlbum, CFileItem* item); + bool GetSourcesBySong(int idSong, const std::string& strPath, CFileItem* item); + int GetSourceByName(const std::string& strSource); + std::string GetSourceById(int id); + + ///////////////////////////////////////////////// + // Genres + ///////////////////////////////////////////////// + int AddGenre(std::string& strGenre); + std::string GetGenreById(int id); + int GetGenreByName(const std::string& strGenre); + + ///////////////////////////////////////////////// + // Link tables + ///////////////////////////////////////////////// + bool AddAlbumArtist(int idArtist, int idAlbum, const std::string& strArtist, int iOrder); + bool GetAlbumsByArtist(int idArtist, std::vector<int>& albums); + bool GetArtistsByAlbum(int idAlbum, CFileItem* item); + bool GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs); + bool DeleteAlbumArtistsByAlbum(int idAlbum); + + int AddRole(const std::string& strRole); + bool AddSongArtist(int idArtist, + int idSong, + const std::string& strRole, + const std::string& strArtist, + int iOrder); + bool AddSongArtist( + int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder); + int AddSongContributor(int idSong, + const std::string& strRole, + const std::string& strArtist, + const std::string& strSort); + void AddSongContributors(int idSong, + const VECMUSICROLES& contributors, + const std::string& strSort); + int GetRoleByName(const std::string& strRole); + bool GetRolesByArtist(int idArtist, CFileItem* item); + bool GetSongsByArtist(int idArtist, std::vector<int>& songs); + bool GetArtistsBySong(int idSong, std::vector<int>& artists); + bool DeleteSongArtistsBySong(int idSong); + + bool AddSongGenres(int idSong, const std::vector<std::string>& genres); + bool GetGenresBySong(int idSong, std::vector<int>& genres); + + bool GetGenresByAlbum(int idAlbum, CFileItem* item); + + bool GetGenresByArtist(int idArtist, CFileItem* item); + bool GetIsAlbumArtist(int idArtist, CFileItem* item); + + ///////////////////////////////////////////////// + // Top 100 + ///////////////////////////////////////////////// + bool GetTop100(const std::string& strBaseDir, CFileItemList& items); + bool GetTop100Albums(VECALBUMS& albums); + bool GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& item); + + ///////////////////////////////////////////////// + // Recently added + ///////////////////////////////////////////////// + bool GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit = 0); + bool GetRecentlyAddedAlbumSongs(const std::string& strBaseDir, + CFileItemList& item, + unsigned int limit = 0); + bool GetRecentlyPlayedAlbums(VECALBUMS& albums); + bool GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, CFileItemList& item); + + ///////////////////////////////////////////////// + // Compilations + ///////////////////////////////////////////////// + int GetCompilationAlbumsCount(); + + //////////////////////////////////////////////// + // Boxsets + //////////////////////////////////////////////// + bool IsAlbumBoxset(int idAlbum); + int GetBoxsetsCount(); + + int GetSinglesCount(); + + int GetArtistCountForRole(int role); + int GetArtistCountForRole(const std::string& strRole); + + /*! \brief Increment the playcount of an item + Increments the playcount and updates the last played date + \param item CFileItem to increment the playcount for + */ + void IncrementPlayCount(const CFileItem& item); + bool CleanupOrphanedItems(); + + ///////////////////////////////////////////////// + // VIEWS + ///////////////////////////////////////////////// + bool GetGenresNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetSourcesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetYearsNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter()); + bool GetRolesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter()); + bool GetArtistsNav(const std::string& strBaseDir, + CFileItemList& items, + bool albumArtistsOnly = false, + int idGenre = -1, + int idAlbum = -1, + int idSong = -1, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetCommonNav(const std::string& strBaseDir, + const std::string& table, + const std::string& labelField, + CFileItemList& items, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */); + bool GetAlbumTypesNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetMusicLabelsNav(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetAlbumsNav(const std::string& strBaseDir, + CFileItemList& items, + int idGenre = -1, + int idArtist = -1, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetDiscsNav(const std::string& strBaseDir, + CFileItemList& items, + int idAlbum, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year); + bool GetSongsNav(const std::string& strBaseDir, + CFileItemList& items, + int idGenre, + int idArtist, + int idAlbum, + const SortDescription& sortDescription = SortDescription()); + bool GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year); + bool GetSongsByWhere(const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription()); + bool GetSongsFullByWhere(const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription(), + bool artistData = false); + bool GetAlbumsByWhere(const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetDiscsByWhere(const std::string& baseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetDiscsByWhere(CMusicDbUrl& musicUrl, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + bool GetArtistsByWhere(const std::string& strBaseDir, + const Filter& filter, + CFileItemList& items, + const SortDescription& sortDescription = SortDescription(), + bool countOnly = false); + int GetDiscsCount(const std::string& baseDir, const Filter& filter = Filter()); + int GetSongsCount(const Filter& filter = Filter()); + bool GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting) override; + int GetOrderFilter(const std::string& type, const SortDescription& sorting, Filter& filter); + + ///////////////////////////////////////////////// + // Party Mode + ///////////////////////////////////////////////// + /*! \brief Gets song IDs in random order that match the filter criteria + \param filter the criteria to apply in the query + \param songIDs a vector of <1, id> pairs suited to party mode use + \return count of song ids found. + */ + unsigned int GetRandomSongIDs(const Filter& filter, std::vector<std::pair<int, int>>& songIDs); + + ///////////////////////////////////////////////// + // JSON-RPC + ///////////////////////////////////////////////// + bool GetGenresJSON(CFileItemList& items, bool bSources = false); + bool GetArtistsByWhereJSON(const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription = SortDescription()); + bool GetAlbumsByWhereJSON(const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription = SortDescription()); + bool GetSongsByWhereJSON(const std::set<std::string>& fields, + const std::string& baseDir, + CVariant& result, + int& total, + const SortDescription& sortDescription = SortDescription()); + + ///////////////////////////////////////////////// + // Scraper + ///////////////////////////////////////////////// + bool SetScraper(int id, const CONTENT_TYPE& content, const ADDON::ScraperPtr& scraper); + bool SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper); + bool GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper); + + /*! \brief Check whether a given scraper is in use. + \param scraperID the scraper to check for. + \return true if the scraper is in use, false otherwise. + */ + bool ScraperInUse(const std::string& scraperID) const; + + ///////////////////////////////////////////////// + // Filters + ///////////////////////////////////////////////// + bool GetItems(const std::string& strBaseDir, + CFileItemList& items, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription()); + bool GetItems(const std::string& strBaseDir, + const std::string& itemType, + CFileItemList& items, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription()); + std::string GetItemById(const std::string& itemType, int id); + + ///////////////////////////////////////////////// + // XML + ///////////////////////////////////////////////// + void ExportToXML(const CLibExportSettings& settings, + CGUIDialogProgress* progressDialog = nullptr); + bool ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog = nullptr); + void ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog = nullptr); + bool ImportSongHistory(const std::string& xmlFile, + const int total, + CGUIDialogProgress* progressDialog = nullptr); + + ///////////////////////////////////////////////// + // Properties + ///////////////////////////////////////////////// + void SetPropertiesForFileItem(CFileItem& item); + static void SetPropertiesFromArtist(CFileItem& item, const CArtist& artist); + static void SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album); + void SetItemUpdated(int mediaId, const std::string& mediaType); + + ///////////////////////////////////////////////// + // Art + ///////////////////////////////////////////////// + /*! \brief Sets art for a database item. + Sets a single piece of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param artType the type of art to set, e.g. "thumb", "fanart" + \param url the url to the art (this is the original url, not a cached url). + \sa GetArtForItem + */ + void SetArtForItem(int mediaId, + const std::string& mediaType, + const std::string& artType, + const std::string& url); + + /*! \brief Sets art for a database item. + Sets multiple pieces of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param art a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art. + \sa GetArtForItem + */ + void SetArtForItem(int mediaId, + const std::string& mediaType, + const std::map<std::string, std::string>& art); + + + /*! \brief Fetch all related art for a database item. + Fetches multiple pieces of art for a database item including that for related media types + Given song id art for the related album, artist(s) and albumartist(s) will also be fetched, looking up the + album and artist when ids are not provided. + Given album id (and not song id) art for the related artist(s) will also be fetched, looking up the + artist(s) when id are not provided. + \param songId the id in the song table, -1 when song art not being fetched + \param albumId the id in the album table, -1 when album art not being fetched + \param artistId the id in the artist table, -1 when artist not known + \param bPrimaryArtist true if art from only the first song artist or album artist is to be fetched + \param art [out] a vector, each element having media type e.g. "artist", "album" or "song", + artType e.g. "thumb", "fanart", etc., prefix of "", "artist" or "albumartist" etc. giving the kind of artist + relationship, and the original url of the art. + + \return true if art is retrieved, false if no art is found. + \sa SetArtForItem + */ + bool GetArtForItem(int songId, + int albumId, + int artistId, + bool bPrimaryArtist, + std::vector<ArtForThumbLoader>& art); + + /*! \brief Fetch art for a database item. + Fetches multiple pieces of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param art [out] a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art. + \return true if art is retrieved, false if no art is found. + \sa SetArtForItem + */ + bool GetArtForItem(int mediaId, + const std::string& mediaType, + std::map<std::string, std::string>& art); + + /*! \brief Fetch art for a database item. + Fetches a single piece of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param artType the type of art to retrieve, eg "thumb", "fanart". + \return the original URL to the piece of art, if available. + \sa SetArtForItem + */ + std::string GetArtForItem(int mediaId, const std::string& mediaType, const std::string& artType); + + /*! \brief Remove art for a database item. + Removes a single piece of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param artType the type of art to remove, eg "thumb", "fanart". + \return true if art is removed, false if no art is found. + \sa RemoveArtForItem + */ + bool RemoveArtForItem(int mediaId, const MediaType& mediaType, const std::string& artType); + + /*! \brief Remove art for a database item. + Removes multiple pieces of art for a database item. + \param mediaId the id in the media (song/artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param arttypes a set of types, e.g. "thumb", "fanart", etc. to be removed. + \return true if art is removed, false if no art is found. + \sa RemoveArtForItem + */ + bool RemoveArtForItem(int mediaId, + const MediaType& mediaType, + const std::set<std::string>& artTypes); + + /*! \brief Fetch the distinct types of art held in the database for a type of media. + \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album). + \param artTypes [out] the types of art e.g. "thumb", "fanart", etc. + \return true if art is found, false if no art is found. + */ + bool GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes); + + /*! \brief Fetch the distinct types of available-but-unassigned art held in the + database for a specific media item. + \param mediaId the id in the media (artist/album) table. + \param mediaType the type of media, which corresponds to the table the item resides in (artist/album). + \return the types of art e.g. "thumb", "fanart", etc. + */ + std::vector<std::string> GetAvailableArtTypesForItem(int mediaId, const MediaType& mediaType); + + /*! \brief Fetch the list of available-but-unassigned art URLs held in the + database for a specific media item and art type. + \param mediaId the id in the media (artist/album) table. + \param mediaType corresponds to the table the item resides in (artist/album). + \param artType e.g. "thumb", "fanart", etc. + \return list of URLs + */ + std::vector<CScraperUrl::SUrlEntry> GetAvailableArtForItem(int mediaId, + const MediaType& mediaType, + const std::string& artType); + + ///////////////////////////////////////////////// + // Tag Scan Version + ///////////////////////////////////////////////// + /*! \brief Check if music files need all tags rescanning regardless of file being unchanged + because the tag processing has changed (which may happen without db version changes) since they + where last scanned. + \return -1 if an error occurred, 0 if no scan is needed, or the version number of tags if not the same as current. + */ + virtual int GetMusicNeedsTagScan(); + + /*! \brief Set minimum version number of db needed when tag data scanned from music files + \param version the version number of db + */ + void SetMusicNeedsTagScan(int version); + + /*! \brief Set the version number of tag data + \param version the version number of db when tags last scanned, 0 (default) means current db version + */ + void SetMusicTagScanVersion(int version = 0); + + std::string GetLibraryLastUpdated(); + void SetLibraryLastUpdated(); + std::string GetLibraryLastCleaned(); + void SetLibraryLastCleaned(); + std::string GetArtistLinksUpdated(); + void SetArtistLinksUpdated(); + std::string GetGenresLastAdded(); + std::string GetSongsLastAdded(); + std::string GetAlbumsLastAdded(); + std::string GetArtistsLastAdded(); + std::string GetSongsLastModified(); + std::string GetAlbumsLastModified(); + std::string GetArtistsLastModified(); + + +protected: + std::map<std::string, int> m_genreCache; + std::map<std::string, int> m_pathCache; + + void CreateTables() override; + void CreateAnalytics() override; + int GetMinSchemaVersion() const override { return 32; } + int GetSchemaVersion() const override; + + const char* GetBaseDBName() const override { return "MyMusic"; } + +private: + /*! \brief (Re)Create the generic database views for songs and albums + */ + virtual void CreateViews(); + void CreateNativeDBFunctions(); + void CreateRemovedLinkTriggers(); + + void SplitPath(const std::string& strFileNameAndPath, + std::string& strPath, + std::string& strFileName); + + CSong GetSongFromDataset(); + CSong GetSongFromDataset(const dbiplus::sql_record* const record, int offset = 0); + CArtist GetArtistFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool needThumb = true); + CArtist GetArtistFromDataset(const dbiplus::sql_record* const record, + int offset = 0, + bool needThumb = true); + CAlbum GetAlbumFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool imageURL = false); + CAlbum GetAlbumFromDataset(const dbiplus::sql_record* const record, + int offset = 0, + bool imageURL = false); + CArtistCredit GetArtistCreditFromDataset(const dbiplus::sql_record* const record, int offset = 0); + CMusicRole GetArtistRoleFromDataset(const dbiplus::sql_record* const record, int offset = 0); + std::string GetMediaDateFromFile(const std::string& strFileNameAndPath); + void GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl); + void GetFileItemFromDataset(const dbiplus::sql_record* const record, + CFileItem* item, + const CMusicDbUrl& baseUrl); + void GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item); + + bool DeleteRemovedLinks(); + + bool CleanupSongs(CGUIDialogProgress* progressDialog = nullptr); + bool CleanupSongsByIds(const std::string& strSongIds); + bool CleanupPaths(); + bool CleanupAlbums(); + bool CleanupArtists(); + bool CleanupGenres(); + bool CleanupInfoSettings(); + bool CleanupRoles(); + void UpdateTables(int version) override; + bool SearchArtists(const std::string& search, CFileItemList& artists); + bool SearchAlbums(const std::string& search, CFileItemList& albums); + bool SearchSongs(const std::string& strSearch, CFileItemList& songs); + int GetSongIDFromPath(const std::string& filePath); + void NormaliseSongDates(std::string& strRelease, std::string& strOriginal); + bool TrimImageURLs(std::string& strImage, const size_t space); + + /*! \brief Build SQL for sort subquery from ignore article token list + \param strField original name or title field that articles could be removed from + \return SQL string e.g. WHEN strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5) + */ + std::string GetIgnoreArticleSQL(const std::string& strField); + + /*! \brief Build SQL for sort name scalar subquery from sort attributes and ignore article list. + \param strAlias alias name of scalar subquery field + \param sortAttributes the sort attributes e.g. SortAttributeIgnoreArticle + \param strField original name or title field that articles could be removed from + \param strSortField sort name or title field to be used instead of original (when data not null) + \return SQL string e.g. + CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort + WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5) + ELSE strField + END AS strAlias + */ + std::string SortnameBuildSQL(const std::string& strAlias, + const SortAttribute& sortAttributes, + const std::string& strField, + const std::string& strSortField); + + /*! \brief Build SQL for sorting field naturally and case-insensitively (in SQLite). + \param strField field name + \param sortOrder the sort order + \return SQL string e.g. + CASE WHEN CAST(strTitle AS INTEGER) = 0 THEN 100000000 + ELSE CAST(strTitle AS INTEGER) END DESC, strTitle COLLATE NOCASE DESC + */ + std::string AlphanumericSortSQL(const std::string& strField, const SortOrder& sortOrder); + + /*! \brief Checks that source table matches sources.xml + returns true when they do + */ + bool CheckSources(VECSOURCES& sources); + + /*! \brief Initially fills source table from sources.xml for use only at + migration of db from an earlier version than 72 + returns true when successfully done + */ + bool MigrateSources(); + + bool m_translateBlankArtist; + + // Fields should be ordered as they + // appear in the songview + static enum _SongFields { + song_idSong = 0, + song_strArtists, + song_strArtistSort, + song_strGenres, + song_strTitle, + song_iTrack, + song_iDuration, + song_strReleaseDate, + song_strOrigReleaseDate, + song_strDiscSubtitle, + song_strFileName, + song_strMusicBrainzTrackID, + song_iTimesPlayed, + song_iStartOffset, + song_iEndOffset, + song_lastplayed, + song_rating, + song_userrating, + song_votes, + song_comment, + song_idAlbum, + song_strAlbum, + song_strPath, + song_strReleaseStatus, + song_bCompilation, + song_bBoxedSet, + song_strAlbumArtists, + song_strAlbumArtistSort, + song_strAlbumReleaseType, + song_mood, + song_strReplayGain, + song_iBPM, + song_iBitRate, + song_iSampleRate, + song_iChannels, + song_iAlbumDuration, + song_iDiscTotal, + song_dateAdded, + song_dateNew, + song_dateModified, + song_enumCount // end of the enum, do not add past here + } SongFields; + + // Fields should be ordered as they + // appear in the albumview + static enum _AlbumFields { + album_idAlbum = 0, + album_strAlbum, + album_strMusicBrainzAlbumID, + album_strReleaseGroupMBID, + album_strArtists, + album_strArtistSort, + album_strGenres, + album_strReleaseDate, + album_strOrigReleaseDate, + album_bBoxedSet, + album_strMoods, + album_strStyles, + album_strThemes, + album_strReview, + album_strLabel, + album_strType, + album_strReleaseStatus, + album_strThumbURL, + album_fRating, + album_iUserrating, + album_iVotes, + album_bCompilation, + album_bScrapedMBID, + album_lastScraped, + album_dateAdded, + album_dateNew, + album_dateModified, + album_iTimesPlayed, + album_strReleaseType, + album_iTotalDiscs, + album_dtLastPlayed, + album_iAlbumDuration, + album_enumCount // end of the enum, do not add past here + } AlbumFields; + + // Fields should be ordered as they + // appear in the songartistview/albumartistview + static enum _ArtistCreditFields { + // used for GetAlbum to get the cascaded album/song artist credits + artistCredit_idEntity = 0, // can be idSong or idAlbum depending on context + artistCredit_idArtist, + artistCredit_idRole, + artistCredit_strRole, + artistCredit_strArtist, + artistCredit_strSortName, + artistCredit_strMusicBrainzArtistID, + artistCredit_iOrder, + artistCredit_enumCount + } ArtistCreditFields; + + // Fields should be ordered as they + // appear in the artistview + static enum _ArtistFields { + artist_idArtist = 0, + artist_strArtist, + artist_strSortName, + artist_strMusicBrainzArtistID, + artist_strType, + artist_strGender, + artist_strDisambiguation, + artist_strBorn, + artist_strFormed, + artist_strGenres, + artist_strMoods, + artist_strStyles, + artist_strInstruments, + artist_strBiography, + artist_strDied, + artist_strDisbanded, + artist_strYearsActive, + artist_strImage, + artist_bScrapedMBID, + artist_lastScraped, + artist_dateAdded, + artist_dateNew, + artist_dateModified, + artist_enumCount // end of the enum, do not add past here + } ArtistFields; + + // Fields fetched by GetArtistsByWhereJSON, order same as in JSONtoDBArtist + static enum _JoinToArtistFields { + joinToArtist_isSong = 0, + joinToArtist_idSourceAlbum, + joinToArtist_idSourceSong, + joinToArtist_idSongGenreAlbum, + joinToArtist_idSongGenreSong, + joinToArtist_strSongGenreAlbum, + joinToArtist_strSongGenreSong, + joinToArtist_idArt, + joinToArtist_artType, + joinToArtist_artURL, + joinToArtist_idRole, + joinToArtist_strRole, + joinToArtist_iOrderRole, + joinToArtist_isalbumartist, + joinToArtist_thumbnail, + joinToArtist_fanart, + joinToArtist_enumCount // end of the enum, do not add past here + } JoinToArtistFields; + + // Fields fetched by GetAlbumsByWhereJSON, order same as in JSONtoDBAlbum + static enum _JoinToAlbumFields { + joinToAlbum_idArtist = 0, + joinToAlbum_strArtist, + joinToAlbum_strArtistMBID, + joinToAlbum_enumCount // end of the enum, do not add past here + } JoinToAlbumFields; + + // Fields fetched by GetSongsByWhereJSON, order same as in JSONtoDBSong + static enum _JoinToSongFields { + // Used by GetSongsByWhereJSON + joinToSongs_idAlbumArtist = 0, + joinToSongs_strAlbumArtist, + joinToSongs_strAlbumArtistMBID, + joinToSongs_iOrderAlbumArtist, + joinToSongs_idArtist, + joinToSongs_strArtist, + joinToSongs_strArtistMBID, + joinToSongs_iOrderArtist, + joinToSongs_idRole, + joinToSongs_strRole, + joinToSongs_iOrderRole, + joinToSongs_idGenre, + joinToSongs_iOrderGenre, + joinToSongs_enumCount // end of the enum, do not add past here + } JoinToSongFields; +}; diff --git a/xbmc/music/MusicDbUrl.cpp b/xbmc/music/MusicDbUrl.cpp new file mode 100644 index 0000000..3b4893b --- /dev/null +++ b/xbmc/music/MusicDbUrl.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicDbUrl.h" + +#include "filesystem/MusicDatabaseDirectory.h" +#include "playlists/SmartPlayList.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +using namespace XFILE; +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CMusicDbUrl::CMusicDbUrl() + : CDbUrl() +{ } + +CMusicDbUrl::~CMusicDbUrl() = default; + +bool CMusicDbUrl::parse() +{ + // the URL must start with musicdb:// + if (!m_url.IsProtocol("musicdb") || m_url.GetFileName().empty()) + return false; + + std::string path = m_url.Get(); + + // Parse path for directory node types and query params + NODE_TYPE dirType; + NODE_TYPE childType; + CQueryParams queryParams; + if (!CMusicDatabaseDirectory::GetDirectoryNodeInfo(path, dirType, childType, queryParams)) + return false; + + switch (dirType) + { + case NODE_TYPE_ARTIST: + m_type = "artists"; + break; + + case NODE_TYPE_ALBUM: + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_TOP100: + m_type = "albums"; + break; + + case NODE_TYPE_DISC: + m_type = "discs"; + break; + + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + case NODE_TYPE_ALBUM_TOP100_SONGS: + case NODE_TYPE_SONG: + case NODE_TYPE_SONG_TOP100: + case NODE_TYPE_SINGLES: + m_type = "songs"; + break; + + default: + break; + } + + switch (childType) + { + case NODE_TYPE_ARTIST: + m_type = "artists"; + break; + + case NODE_TYPE_ALBUM: + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_TOP100: + m_type = "albums"; + break; + + case NODE_TYPE_DISC: + m_type = "discs"; + break; + + case NODE_TYPE_SONG: + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + case NODE_TYPE_ALBUM_TOP100_SONGS: + case NODE_TYPE_SONG_TOP100: + case NODE_TYPE_SINGLES: + m_type = "songs"; + break; + + case NODE_TYPE_GENRE: + m_type = "genres"; + break; + + case NODE_TYPE_SOURCE: + m_type = "sources"; + break; + + case NODE_TYPE_ROLE: + m_type = "roles"; + break; + + case NODE_TYPE_YEAR: + m_type = "years"; + break; + + case NODE_TYPE_TOP100: + m_type = "top100"; + break; + + case NODE_TYPE_ROOT: + case NODE_TYPE_OVERVIEW: + default: + return false; + } + + if (m_type.empty()) + return false; + + // retrieve and parse all options + AddOptions(m_url.GetOptions()); + + // add options based on the node type + if (dirType == NODE_TYPE_SINGLES || childType == NODE_TYPE_SINGLES) + AddOption("singles", true); + + // add options based on the QueryParams + if (queryParams.GetArtistId() != -1) + AddOption("artistid", (int)queryParams.GetArtistId()); + if (queryParams.GetAlbumId() != -1) + AddOption("albumid", (int)queryParams.GetAlbumId()); + if (queryParams.GetGenreId() != -1) + AddOption("genreid", (int)queryParams.GetGenreId()); + if (queryParams.GetSongId() != -1) + AddOption("songid", (int)queryParams.GetSongId()); + if (queryParams.GetYear() != -1) + AddOption("year", (int)queryParams.GetYear()); + + // Decode legacy use of "musicdb://compilations/" path for filtered albums + if (m_url.GetFileName() == "compilations/") + AddOption("compilation", true); + + return true; +} + +bool CMusicDbUrl::validateOption(const std::string &key, const CVariant &value) +{ + if (!CDbUrl::validateOption(key, value)) + return false; + + // if the value is empty it will remove the option which is ok + // otherwise we only care about the "filter" option here + if (value.empty() || !StringUtils::EqualsNoCase(key, "filter")) + return true; + + if (!value.isString()) + return false; + + CSmartPlaylist xspFilter; + if (!xspFilter.LoadFromJson(value.asString())) + return false; + + // check if the filter playlist matches the item type + return xspFilter.GetType() == m_type; +} diff --git a/xbmc/music/MusicDbUrl.h b/xbmc/music/MusicDbUrl.h new file mode 100644 index 0000000..06f3efe --- /dev/null +++ b/xbmc/music/MusicDbUrl.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DbUrl.h" + +#include <string> + +class CVariant; + +class CMusicDbUrl : public CDbUrl +{ +public: + CMusicDbUrl(); + ~CMusicDbUrl() override; + +protected: + bool parse() override; + bool validateOption(const std::string &key, const CVariant &value) override; +}; diff --git a/xbmc/music/MusicInfoLoader.cpp b/xbmc/music/MusicInfoLoader.cpp new file mode 100644 index 0000000..2732bd4 --- /dev/null +++ b/xbmc/music/MusicInfoLoader.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoLoader.h" + +#include "Album.h" +#include "Artist.h" +#include "FileItem.h" +#include "MusicDatabase.h" +#include "MusicThumbLoader.h" +#include "ServiceBroker.h" +#include "filesystem/File.h" +#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h" +#include "filesystem/MusicDatabaseDirectory/QueryParams.h" +#include "music/tags/MusicInfoTag.h" +#include "music/tags/MusicInfoTagLoaderFactory.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Archive.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace XFILE; +using namespace MUSIC_INFO; + +// HACK until we make this threadable - specify 1 thread only for now +CMusicInfoLoader::CMusicInfoLoader() + : CBackgroundInfoLoader() + , m_databaseHits{0} + , m_tagReads{0} +{ + m_mapFileItems = new CFileItemList; + + m_thumbLoader = new CMusicThumbLoader(); +} + +CMusicInfoLoader::~CMusicInfoLoader() +{ + StopThread(); + delete m_mapFileItems; + delete m_thumbLoader; +} + +void CMusicInfoLoader::OnLoaderStart() +{ + // Load previously cached items from HD + if (!m_strCacheFileName.empty()) + LoadCache(m_strCacheFileName, *m_mapFileItems); + else + { + m_mapFileItems->SetPath(m_pVecItems->GetPath()); + m_mapFileItems->Load(); + m_mapFileItems->SetFastLookup(true); + } + + m_strPrevPath.clear(); + + m_databaseHits = m_tagReads = 0; + + if (m_pProgressCallback) + m_pProgressCallback->SetProgressMax(m_pVecItems->GetFileCount()); + + m_musicDatabase.Open(); + + if (m_thumbLoader) + m_thumbLoader->OnLoaderStart(); +} + +bool CMusicInfoLoader::LoadAdditionalTagInfo(CFileItem* pItem) +{ + if (!pItem || (pItem->m_bIsFolder && !pItem->IsAudio()) || + pItem->IsPlayList() || pItem->IsNFO() || pItem->IsInternetStream()) + return false; + + if (pItem->GetProperty("hasfullmusictag") == "true") + return false; // already have the information + + std::string path(pItem->GetPath()); + // For songs in library set the (primary) song artist and album properties + // Use song Id (not path) as called for items from either library or file view, + // but could also be listitem with tag loaded by a script + if (pItem->HasMusicInfoTag() && + pItem->GetMusicInfoTag()->GetType() == MediaTypeSong && + pItem->GetMusicInfoTag()->GetDatabaseId() > 0) + { + CMusicDatabase database; + database.Open(); + // May already have song artist ids as item property set when data read from + // db, but check property is valid array (scripts could set item properties + // incorrectly), otherwise fetch artist using song id. + CArtist artist; + bool artistfound = false; + if (pItem->HasProperty("artistid") && pItem->GetProperty("artistid").isArray()) + { + CVariant::const_iterator_array varid = pItem->GetProperty("artistid").begin_array(); + int idArtist = static_cast<int>(varid->asInteger()); + artistfound = database.GetArtist(idArtist, artist, false); + } + else + artistfound = database.GetArtistFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), artist); + if (artistfound) + CMusicDatabase::SetPropertiesFromArtist(*pItem, artist); + + // May already have album id, otherwise fetch album from song id + CAlbum album; + bool albumfound = false; + int idAlbum = pItem->GetMusicInfoTag()->GetAlbumId(); + if (idAlbum > 0) + albumfound = database.GetAlbum(idAlbum, album, false); + else + albumfound = database.GetAlbumFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), album); + if (albumfound) + CMusicDatabase::SetPropertiesFromAlbum(*pItem, album); + + path = pItem->GetMusicInfoTag()->GetURL(); + } + + CLog::Log(LOGDEBUG, "Loading additional tag info for file {}", path); + + // we load up the actual tag for this file in order to + // fetch the lyrics and add it to the current music info tag + CFileItem tempItem(path, false); + std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(tempItem)); + if (nullptr != pLoader) + { + CMusicInfoTag tag; + pLoader->Load(path, tag); + pItem->GetMusicInfoTag()->SetLyrics(tag.GetLyrics()); + pItem->SetProperty("hasfullmusictag", "true"); + return true; + } + return false; +} + +bool CMusicInfoLoader::LoadItem(CFileItem* pItem) +{ + bool result = LoadItemCached(pItem); + result |= LoadItemLookup(pItem); + + return result; +} + +bool CMusicInfoLoader::LoadItemCached(CFileItem* pItem) +{ + if ((pItem->m_bIsFolder && !pItem->IsAudio()) || + pItem->IsPlayList() || pItem->IsSmartPlayList() || + StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") || + StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") || + pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb())) + return false; + + // Get thumb for item + m_thumbLoader->LoadItem(pItem); + + return true; +} + +bool CMusicInfoLoader::LoadItemLookup(CFileItem* pItem) +{ + if (m_pProgressCallback && !pItem->m_bIsFolder) + m_pProgressCallback->SetProgressAdvance(); + + if ((pItem->m_bIsFolder && !pItem->IsAudio()) || // + pItem->IsPlayList() || pItem->IsSmartPlayList() || // + StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") || // + StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") || // + pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb())) + return false; + + if ((!pItem->HasMusicInfoTag() || !pItem->GetMusicInfoTag()->Loaded()) && pItem->IsAudio()) + { + // first check the cached item + CFileItemPtr mapItem = (*m_mapFileItems)[pItem->GetPath()]; + if (mapItem && mapItem->m_dateTime==pItem->m_dateTime && mapItem->HasMusicInfoTag() && mapItem->GetMusicInfoTag()->Loaded()) + { // Query map if we previously cached the file on HD + *pItem->GetMusicInfoTag() = *mapItem->GetMusicInfoTag(); + if (mapItem->HasArt("thumb")) + pItem->SetArt("thumb", mapItem->GetArt("thumb")); + } + else + { + std::string strPath = URIUtils::GetDirectory(pItem->GetPath()); + URIUtils::AddSlashAtEnd(strPath); + if (strPath!=m_strPrevPath) + { + // The item is from another directory as the last one, + // query the database for the new directory... + m_musicDatabase.GetSongsByPath(strPath, m_songsMap); + m_databaseHits++; + } + + /* + This only loads the item with the song from the database when it maps to a single song, + it can not load song data for items with cuesheets that expand to multiple songs. + For songs from embedded or separate cuesheets strFileName is not unique, so the song map for + the path will have the list of songs from that file. But items with cuesheets are expanded + (replacing each item with items for every track) elsewhere. When the item we are looking up + has a cuesheet document or is a music file with a cuesheet embedded in the tags, and it maps + to more than one song then we can not fill the tag data and thumb from the database. + */ + MAPSONGS::iterator it = m_songsMap.find(pItem->GetPath()); // Find file in song map + if (it != m_songsMap.end() && it->second.size() == 1) + { + // Have we loaded this item from database before, + // and even if it has a cuesheet it has only one song + pItem->GetMusicInfoTag()->SetSong(it->second[0]); + if (!it->second[0].strThumb.empty()) + pItem->SetArt("thumb", it->second[0].strThumb); + } + else if (pItem->IsMusicDb()) + { // a music db item that doesn't have tag loaded - grab details from the database + XFILE::MUSICDATABASEDIRECTORY::CQueryParams param; + XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(pItem->GetPath(),param); + CSong song; + if (m_musicDatabase.GetSong(param.GetSongId(), song)) + { + pItem->GetMusicInfoTag()->SetSong(song); + if (!song.strThumb.empty()) + pItem->SetArt("thumb", song.strThumb); + } + } + else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) || pItem->IsCDDA()) + { // Nothing found, load tag from file, + // always try to load cddb info + // get correct tag parser + std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem)); + if (nullptr != pLoader) + // get tag + pLoader->Load(pItem->GetPath(), *pItem->GetMusicInfoTag()); + m_tagReads++; + } + + m_strPrevPath = strPath; + } + } + + return true; +} + +void CMusicInfoLoader::OnLoaderFinish() +{ + // cleanup last loaded songs from database + m_songsMap.clear(); + + // cleanup cache loaded from HD + m_mapFileItems->Clear(); + + // Save loaded items to HD + if (!m_strCacheFileName.empty()) + SaveCache(m_strCacheFileName, *m_pVecItems); + else if (!m_bStop && (m_databaseHits > 1 || m_tagReads > 0)) + m_pVecItems->Save(); + + m_musicDatabase.Close(); + + if (m_thumbLoader) + m_thumbLoader->OnLoaderFinish(); +} + +void CMusicInfoLoader::UseCacheOnHD(const std::string& strFileName) +{ + m_strCacheFileName = strFileName; +} + +void CMusicInfoLoader::LoadCache(const std::string& strFileName, CFileItemList& items) +{ + CFile file; + + if (file.Open(strFileName)) + { + CArchive ar(&file, CArchive::load); + int iSize = 0; + ar >> iSize; + for (int i = 0; i < iSize; i++) + { + CFileItemPtr pItem(new CFileItem()); + ar >> *pItem; + items.Add(pItem); + } + ar.Close(); + file.Close(); + items.SetFastLookup(true); + } +} + +void CMusicInfoLoader::SaveCache(const std::string& strFileName, CFileItemList& items) +{ + int iSize = items.Size(); + + if (iSize <= 0) + return ; + + CFile file; + + if (file.OpenForWrite(strFileName)) + { + CArchive ar(&file, CArchive::store); + ar << items.Size(); + for (int i = 0; i < iSize; i++) + { + CFileItemPtr pItem = items[i]; + ar << *pItem; + } + ar.Close(); + file.Close(); + } + +} diff --git a/xbmc/music/MusicInfoLoader.h b/xbmc/music/MusicInfoLoader.h new file mode 100644 index 0000000..d6cab41 --- /dev/null +++ b/xbmc/music/MusicInfoLoader.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "BackgroundInfoLoader.h" +#include "MusicDatabase.h" + +class CFileItemList; +class CMusicThumbLoader; + +namespace MUSIC_INFO +{ +class CMusicInfoLoader : public CBackgroundInfoLoader +{ +public: + CMusicInfoLoader(); + ~CMusicInfoLoader() override; + + void UseCacheOnHD(const std::string& strFileName); + bool LoadItem(CFileItem* pItem) override; + bool LoadItemCached(CFileItem* pItem) override; + bool LoadItemLookup(CFileItem* pItem) override; + static bool LoadAdditionalTagInfo(CFileItem* pItem); + +protected: + void OnLoaderStart() override; + void OnLoaderFinish() override; + void LoadCache(const std::string& strFileName, CFileItemList& items); + void SaveCache(const std::string& strFileName, CFileItemList& items); +protected: + std::string m_strCacheFileName; + CFileItemList* m_mapFileItems; + MAPSONGS m_songsMap; + std::string m_strPrevPath; + CMusicDatabase m_musicDatabase; + unsigned int m_databaseHits; + unsigned int m_tagReads; + CMusicThumbLoader *m_thumbLoader; +}; +} diff --git a/xbmc/music/MusicLibraryQueue.cpp b/xbmc/music/MusicLibraryQueue.cpp new file mode 100644 index 0000000..f16272f --- /dev/null +++ b/xbmc/music/MusicLibraryQueue.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryQueue.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "dialogs/GUIDialogProgress.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/jobs/MusicLibraryCleaningJob.h" +#include "music/jobs/MusicLibraryExportJob.h" +#include "music/jobs/MusicLibraryImportJob.h" +#include "music/jobs/MusicLibraryJob.h" +#include "music/jobs/MusicLibraryScanningJob.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Variant.h" + +#include <mutex> +#include <utility> + +CMusicLibraryQueue::CMusicLibraryQueue() + : CJobQueue(false, 1, CJob::PRIORITY_LOW), + m_jobs() +{ } + +CMusicLibraryQueue::~CMusicLibraryQueue() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + m_jobs.clear(); +} + +CMusicLibraryQueue& CMusicLibraryQueue::GetInstance() +{ + static CMusicLibraryQueue s_instance; + return s_instance; +} + +void CMusicLibraryQueue::ExportLibrary(const CLibExportSettings& settings, bool showDialog /* = false */) +{ + CGUIDialogProgress* progress = NULL; + if (showDialog) + { + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{ 20196 }); //"Export music library" + progress->SetText(CVariant{ 650 }); //"Exporting" + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + } + + CMusicLibraryExportJob* exportJob = new CMusicLibraryExportJob(settings, progress); + if (showDialog) + { + AddJob(exportJob); + + // Wait for export to complete or be canceled, but render every 10ms so that the + // pointer movements work on dialog even when export is reporting progress infrequently + if (progress) + progress->Wait(); + } + else + { + m_modal = true; + exportJob->DoWork(); + + delete exportJob; + m_modal = false; + Refresh(); + } +} + +void CMusicLibraryQueue::ImportLibrary(const std::string& xmlFile, bool showDialog /* = false */) +{ + CGUIDialogProgress* progress = nullptr; + if (showDialog) + { + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{ 20197 }); //"Import music library" + progress->SetText(CVariant{ 649 }); //"Importing" + progress->SetLine(1, CVariant{ 330 }); //"This could take some time" + progress->SetLine(2, CVariant{ "" }); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + } + + CMusicLibraryImportJob* importJob = new CMusicLibraryImportJob(xmlFile, progress); + if (showDialog) + { + AddJob(importJob); + + // Wait for import to complete or be canceled, but render every 10ms so that the + // pointer movements work on dialog even when import is reporting progress infrequently + if (progress) + progress->Wait(); + } + else + { + m_modal = true; + importJob->DoWork(); + + delete importJob; + m_modal = false; + Refresh(); + } +} + +void CMusicLibraryQueue::ScanLibrary(const std::string& strDirectory, + int flags /* = 0 */, + bool showProgress /* = true */) +{ + if (flags == MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO)) + flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_ONLINE; + } + + if (!showProgress || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE)) + flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_BACKGROUND; + + AddJob(new CMusicLibraryScanningJob(strDirectory, flags, showProgress)); +} + +void CMusicLibraryQueue::StartAlbumScan(const std::string & strDirectory, bool refresh) +{ + int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS; + if (refresh) + flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN; + AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true)); +} + +void CMusicLibraryQueue::StartArtistScan(const std::string& strDirectory, bool refresh) +{ + int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS; + if (refresh) + flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN; + AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true)); +} + +bool CMusicLibraryQueue::IsScanningLibrary() const +{ + // check if the library is being cleaned synchronously + if (m_cleaning) + return true; + + // check if the library is being scanned asynchronously + MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob"); + if (scanningJobs != m_jobs.end() && !scanningJobs->second.empty()) + return true; + + // check if the library is being cleaned asynchronously + MusicLibraryJobMap::const_iterator cleaningJobs = m_jobs.find("MusicLibraryCleaningJob"); + if (cleaningJobs != m_jobs.end() && !cleaningJobs->second.empty()) + return true; + + return false; +} + +void CMusicLibraryQueue::StopLibraryScanning() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob"); + if (scanningJobs == m_jobs.end()) + return; + + // get a copy of the scanning jobs because CancelJob() will modify m_scanningJobs + MusicLibraryJobs tmpScanningJobs(scanningJobs->second.begin(), scanningJobs->second.end()); + + // cancel all scanning jobs + for (const auto& job : tmpScanningJobs) + CancelJob(job); + Refresh(); +} + +void CMusicLibraryQueue::CleanLibrary(bool showDialog /* = false */) +{ + CGUIDialogProgress* progress = NULL; + if (showDialog) + { + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{ 700 }); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + } + + CMusicLibraryCleaningJob* cleaningJob = new CMusicLibraryCleaningJob(progress); + AddJob(cleaningJob); + + // Wait for cleaning to complete or be canceled, but render every 20ms so that the + // pointer movements work on dialog even when cleaning is reporting progress infrequently + if (progress) + progress->Wait(20); +} + +void CMusicLibraryQueue::AddJob(CMusicLibraryJob *job) +{ + if (job == NULL) + return; + + std::unique_lock<CCriticalSection> lock(m_critical); + if (!CJobQueue::AddJob(job)) + return; + + // add the job to our list of queued/running jobs + std::string jobType = job->GetType(); + MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType); + if (jobsIt == m_jobs.end()) + { + MusicLibraryJobs jobs; + jobs.insert(job); + m_jobs.insert(std::make_pair(jobType, jobs)); + } + else + jobsIt->second.insert(job); +} + +void CMusicLibraryQueue::CancelJob(CMusicLibraryJob *job) +{ + if (job == NULL) + return; + + std::unique_lock<CCriticalSection> lock(m_critical); + // remember the job type needed later because the job might be deleted + // in the call to CJobQueue::CancelJob() + std::string jobType; + if (job->GetType() != NULL) + jobType = job->GetType(); + + // check if the job supports cancellation and cancel it + if (job->CanBeCancelled()) + job->Cancel(); + + // remove the job from the job queue + CJobQueue::CancelJob(job); + + // remove the job from our list of queued/running jobs + MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType); + if (jobsIt != m_jobs.end()) + jobsIt->second.erase(job); +} + +void CMusicLibraryQueue::CancelAllJobs() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + CJobQueue::CancelJobs(); + + // remove all scanning jobs + m_jobs.clear(); +} + +bool CMusicLibraryQueue::IsRunning() const +{ + return CJobQueue::IsProcessing() || m_modal; +} + +void CMusicLibraryQueue::Refresh() +{ + CUtil::DeleteMusicDatabaseDirectoryCache(); + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CMusicLibraryQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job) +{ + if (success) + { + if (QueueEmpty()) + Refresh(); + } + + { + std::unique_lock<CCriticalSection> lock(m_critical); + // remove the job from our list of queued/running jobs + MusicLibraryJobMap::iterator jobsIt = m_jobs.find(job->GetType()); + if (jobsIt != m_jobs.end()) + jobsIt->second.erase(static_cast<CMusicLibraryJob*>(job)); + } + + return CJobQueue::OnJobComplete(jobID, success, job); +} diff --git a/xbmc/music/MusicLibraryQueue.h b/xbmc/music/MusicLibraryQueue.h new file mode 100644 index 0000000..f7948f7 --- /dev/null +++ b/xbmc/music/MusicLibraryQueue.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "settings/LibExportSettings.h" +#include "threads/CriticalSection.h" +#include "utils/JobManager.h" + +#include <map> +#include <set> + +class CGUIDialogProgressBarHandle; +class CMusicLibraryJob; +class CGUIDialogProgress; + +/*! + \brief Queue for music library jobs. + + The queue can only process a single job at any time and every job will be + executed at the lowest priority. + */ +class CMusicLibraryQueue : protected CJobQueue +{ +public: + ~CMusicLibraryQueue() override; + + /*! + \brief Gets the singleton instance of the music library queue. + */ + static CMusicLibraryQueue& GetInstance(); + + /*! + \brief Enqueue a music library export job. + \param[in] settings Library export settings + \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous + */ + void ExportLibrary(const CLibExportSettings& settings, bool showDialog = false); + + /*! + \brief Enqueue a music library import job. + \param[in] xmlFile xml file to import + \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous + */ + void ImportLibrary(const std::string& xmlFile, bool showDialog = false); + + /*! + \brief Enqueue a music library update job, scanning tags embedded in music files and optionally scraping additional data. + \param[in] strDirectory Directory to scan or "" (empty string) for a global scan. + \param[in] flags Flags for controlling the scanning process. See xbmc/music/infoscanner/MusicInfoScanner.h for possible values. + \param[in] showProgress Whether or not to show a progress dialog. Defaults to true + */ + void ScanLibrary(const std::string& strDirectory, int flags = 0, bool showProgress = true); + + /*! + \brief Enqueue an album scraping job fetching additional album data. + \param[in] strDirectory Virtual path that identifies which albums to process or "" (empty string) for all albums + \param[in] refresh Whether or not to refresh data for albums that have previously been scraped + */ + void StartAlbumScan(const std::string& strDirectory, bool refresh = false); + + /*! + \brief Enqueue an artist scraping job fetching additional artist data. + \param[in] strDirectory Virtual path that identifies which artists to process or "" (empty string) for all artists + \param[in] refresh Whether or not to refresh data for artists that have previously been scraped + */ + void StartArtistScan(const std::string& strDirectory, bool refresh = false); + + /*! + \brief Check if a library scan or cleaning is in progress. + \return True if a scan or clean is in progress, false otherwise + */ + bool IsScanningLibrary() const; + + /*! + \brief Stop and dequeue all scanning jobs. + */ + void StopLibraryScanning(); + + /*! + \brief Enqueue an asynchronous library cleaning job. + \param[in] showDialog Show a model progress dialog while cleaning. Default is false. + */ + void CleanLibrary(bool showDialog = false); + + /*! + \brief Adds the given job to the queue. + \param[in] job Music library job to be queued. + */ + void AddJob(CMusicLibraryJob *job); + + /*! + \brief Cancels the given job and removes it from the queue. + \param[in] job Music library job to be canceled and removed from the queue. + */ + void CancelJob(CMusicLibraryJob *job); + + /*! + \brief Cancels all running and queued jobs. + */ + void CancelAllJobs(); + + /*! + \brief Whether any jobs are running or not. + */ + bool IsRunning() const; + +protected: + // implementation of IJobCallback + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + + /*! + \brief Notifies all to refresh the current listings. + */ + void Refresh(); + +private: + CMusicLibraryQueue(); + CMusicLibraryQueue(const CMusicLibraryQueue&); + CMusicLibraryQueue const& operator=(CMusicLibraryQueue const&); + + typedef std::set<CMusicLibraryJob*> MusicLibraryJobs; + typedef std::map<std::string, MusicLibraryJobs> MusicLibraryJobMap; + MusicLibraryJobMap m_jobs; + CCriticalSection m_critical; + + bool m_modal = false; + bool m_exporting = false; + bool m_cleaning = false; +}; diff --git a/xbmc/music/MusicThumbLoader.cpp b/xbmc/music/MusicThumbLoader.cpp new file mode 100644 index 0000000..98d369a --- /dev/null +++ b/xbmc/music/MusicThumbLoader.cpp @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicThumbLoader.h" + +#include "FileItem.h" +#include "TextureDatabase.h" +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/tags/MusicInfoTag.h" +#include "music/tags/MusicInfoTagLoaderFactory.h" +#include "utils/StringUtils.h" +#include "video/VideoThumbLoader.h" + +#include <utility> + +using namespace MUSIC_INFO; + +CMusicThumbLoader::CMusicThumbLoader() : CThumbLoader() +{ + m_musicDatabase = new CMusicDatabase; +} + +CMusicThumbLoader::~CMusicThumbLoader() +{ + delete m_musicDatabase; +} + +void CMusicThumbLoader::OnLoaderStart() +{ + m_musicDatabase->Open(); + m_albumArt.clear(); + CThumbLoader::OnLoaderStart(); +} + +void CMusicThumbLoader::OnLoaderFinish() +{ + m_musicDatabase->Close(); + m_albumArt.clear(); + CThumbLoader::OnLoaderFinish(); +} + +bool CMusicThumbLoader::LoadItem(CFileItem* pItem) +{ + bool result = LoadItemCached(pItem); + result |= LoadItemLookup(pItem); + + return result; +} + +bool CMusicThumbLoader::LoadItemCached(CFileItem* pItem) +{ + if (pItem->m_bIsShareOrDrive) + return false; + + if (pItem->HasMusicInfoTag() && !pItem->GetProperty("libraryartfilled").asBoolean()) + { + if (FillLibraryArt(*pItem)) + return true; + + if (pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist) + return false; // No fallback + } + + if (pItem->HasVideoInfoTag() && !pItem->HasArt("thumb")) + { // music video + CVideoThumbLoader loader; + if (loader.LoadItemCached(pItem)) + return true; + } + + // Fallback to folder thumb when path has one cached + if (!pItem->HasArt("thumb")) + { + std::string art = GetCachedImage(*pItem, "thumb"); + if (!art.empty()) + pItem->SetArt("thumb", art); + } + + // Fallback to folder fanart when path has one cached + //! @todo Remove as "fanart" is never been cached for music folders (only for + // artists) or start caching fanart for folders? + if (!pItem->HasArt("fanart")) + { + std::string art = GetCachedImage(*pItem, "fanart"); + if (!art.empty()) + { + pItem->SetArt("fanart", art); + } + } + + return false; +} + +bool CMusicThumbLoader::LoadItemLookup(CFileItem* pItem) +{ + if (pItem->m_bIsShareOrDrive) + return false; + + if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist) // No fallback for artist + return false; + + if (pItem->HasVideoInfoTag()) + { // music video + CVideoThumbLoader loader; + if (loader.LoadItemLookup(pItem)) + return true; + } + + if (!pItem->HasArt("thumb")) + { + // Look for embedded art + if (pItem->HasMusicInfoTag() && !pItem->GetMusicInfoTag()->GetCoverArtInfo().Empty()) + { + // The item has got embedded art but user thumbs overrule, so check for those first + if (!FillThumb(*pItem, false)) // Check for user thumbs but ignore folder thumbs + { + // No user thumb, use embedded art + std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "music"); + pItem->SetArt("thumb", thumb); + } + } + else + { + // Check for user thumbs + FillThumb(*pItem, true); + } + } + + return true; +} + +bool CMusicThumbLoader::FillThumb(CFileItem &item, bool folderThumbs /* = true */) +{ + if (item.HasArt("thumb")) + return true; + std::string thumb = GetCachedImage(item, "thumb"); + if (thumb.empty()) + { + thumb = item.GetUserMusicThumb(false, folderThumbs); + if (!thumb.empty()) + SetCachedImage(item, "thumb", thumb); + } + if (!thumb.empty()) + item.SetArt("thumb", thumb); + return !thumb.empty(); +} + +bool CMusicThumbLoader::FillLibraryArt(CFileItem &item) +{ + /* Called for any item with MusicInfoTag and no art. + Items on Genres, Sources and Roles nodes have ID (although items on Years + node do not) so check for song/album/artist specifically. + Non-library songs (file view) can also have MusicInfoTag but no ID or type + */ + bool artfound(false); + std::vector<ArtForThumbLoader> art; + CMusicInfoTag &tag = *item.GetMusicInfoTag(); + if (tag.GetDatabaseId() > -1 && + (tag.GetType() == MediaTypeSong || tag.GetType() == MediaTypeAlbum || + tag.GetType() == MediaTypeArtist)) + { + // Item in music library, fetch the art + m_musicDatabase->Open(); + if (tag.GetType() == MediaTypeSong) + artfound = m_musicDatabase->GetArtForItem(tag.GetDatabaseId(), tag.GetAlbumId(), -1, false, art); + else if (tag.GetType() == MediaTypeAlbum) + artfound = m_musicDatabase->GetArtForItem(-1, tag.GetDatabaseId(), -1, false, art); + else //Artist + artfound = m_musicDatabase->GetArtForItem(-1, -1, tag.GetDatabaseId(), true, art); + + m_musicDatabase->Close(); + } + else if (!tag.GetArtist().empty() && + (tag.GetType() == MediaTypeNone || tag.GetType() == MediaTypeSong)) + { + /* + Could be non-library song - has musictag but no ID or type (may have + thumb already). Try to fetch both song artist(s) and album artist(s) art by + artist name, e.g. "artist.thumb", "artist.fanart", "artist.clearlogo", + "artist.banner", "artist1.thumb", "artist1.fanart", "artist1.clearlogo", + "artist1.banner", "albumartist.thumb", "albumartist.fanart" etc. + Set fanart as fallback. + */ + CSong song; + // Try to split song artist names (various tags) into artist credits + song.SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID()); + if (!song.artistCredits.empty()) + { + tag.SetType(MediaTypeSong); // Makes "Information" context menu visible + m_musicDatabase->Open(); + int iOrder = 0; + // Song artist art + for (const auto& artistCredit : song.artistCredits) + { + int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist()); + if (idArtist > 0) + { + std::vector<ArtForThumbLoader> artistart; + if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart)) + { + for (auto& artitem : artistart) + { + if (iOrder > 0) + artitem.prefix = StringUtils::Format("artist{}", iOrder); + else + artitem.prefix = "artist"; + } + art.insert(art.end(), artistart.begin(), artistart.end()); + } + } + ++iOrder; + } + // Album artist art + if (!tag.GetAlbumArtist().empty() && tag.GetArtistString().compare(tag.GetAlbumArtistString()) != 0) + { + // Split song artist names correctly into artist credits from various tag + // arrays, inc. fallback to song artist names + CAlbum album; + album.SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(), + tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID()); + + iOrder = 0; + for (const auto& artistCredit : album.artistCredits) + { + int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist()); + if (idArtist > 0) + { + std::vector<ArtForThumbLoader> artistart; + if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart)) + { + for (auto& artitem : artistart) + { + if (iOrder > 0) + artitem.prefix = StringUtils::Format("albumartist{}", iOrder); + else + artitem.prefix = "albumartist"; + } + art.insert(art.end(), artistart.begin(), artistart.end()); + } + } + ++iOrder; + } + } + else + { + // Replicate the artist art as album artist art + std::vector<ArtForThumbLoader> artistart; + for (const auto& artitem : art) + { + ArtForThumbLoader newart; + newart.artType = artitem.artType; + newart.mediaType = artitem.mediaType; + newart.prefix = "album" + artitem.prefix; + newart.url = artitem.url; + artistart.emplace_back(newart); + } + art.insert(art.end(), artistart.begin(), artistart.end()); + } + artfound = !art.empty(); + m_musicDatabase->Close(); + } + } + + if (artfound) + { + std::string fanartfallback; + std::string artname; + std::map<std::string, std::string> artmap; + std::map<std::string, std::string> discartmap; + for (auto artitem : art) + { + /* Add art to artmap, naming according to media type. + For example: artists have "thumb", "fanart", "poster" etc., + albums have "thumb", "artist.thumb", "artist.fanart",... "artist1.thumb", "artist1.fanart" etc., + songs have "thumb", "album.thumb", "artist.thumb", "albumartist.thumb", "albumartist1.thumb" etc. + */ + if (tag.GetType() == artitem.mediaType) + artname = artitem.artType; + else if (artitem.prefix.empty()) + artname = artitem.mediaType + "." + artitem.artType; + else + { + if (tag.GetType() == MediaTypeAlbum) + StringUtils::Replace(artitem.prefix, "albumartist", "artist"); + artname = artitem.prefix + "." + artitem.artType; + } + + // Pull out album art for this specific disc e.g. "thumb2", skip art for other discs + if (artitem.mediaType == MediaTypeAlbum && tag.GetDiscNumber() > 0) + { + // Find any trailing digits + size_t startnum = artitem.artType.find_last_not_of("0123456789"); + std::string digits = artitem.artType.substr(startnum + 1); + int num = atoi(digits.c_str()); + if (num > 0 && startnum < artitem.artType.size()) + { + if (num == tag.GetDiscNumber()) + discartmap.insert(std::make_pair(artitem.artType.substr(0, startnum + 1), artitem.url)); + continue; + } + } + + artmap.insert(std::make_pair(artname, artitem.url)); + + // Add fallback art for "thumb" and "fanart" art types only + // Set album thumb as the fallback used when song thumb is missing + if (tag.GetType() == MediaTypeSong && artitem.mediaType == MediaTypeAlbum && + artitem.artType == "thumb") + { + item.SetArtFallback(artitem.artType, artname); + } + + // For albums and songs set fallback fanart from the artist. + // For songs prefer primary song artist over primary albumartist fanart as fallback fanart + if (artitem.prefix == "artist" && artitem.artType == "fanart") + fanartfallback = artname; + if (artitem.prefix == "albumartist" && artitem.artType == "fanart" && fanartfallback.empty()) + fanartfallback = artname; + } + if (!fanartfallback.empty()) + item.SetArtFallback("fanart", fanartfallback); + + // Process specific disc art when we have some + for (const auto& discart : discartmap) + { + std::map<std::string, std::string>::iterator it; + if (tag.GetType() == MediaTypeAlbum) + { + // Insert or replace album art with specific disc art + it = artmap.find(discart.first); + if (it != artmap.end()) + it->second = discart.second; + else + artmap.insert(discart); + } + else if (tag.GetType() == MediaTypeSong) + { + // Use disc thumb rather than album as fallback for song thumb + // (Fallback approach is used to fill missing thumbs). + if (discart.first == "thumb") + { + it = artmap.find("album.thumb"); + if (it != artmap.end()) + // Replace "album.thumb" already set as fallback + it->second = discart.second; + else + { + // Insert thumb for album and set as fallback + artmap.insert(std::make_pair("album.thumb", discart.second)); + item.SetArtFallback("thumb", "album.thumb"); + } + } + else + { + // Apply disc art as song art when not have that type (fallback does not apply). + // Art of other types could been set via JSON, or in future read from metadata + it = artmap.find(discart.first); + if (it == artmap.end()) + artmap.insert(discart); + } + } + } + + item.AppendArt(artmap); + } + + item.SetProperty("libraryartfilled", true); + return artfound; +} + +bool CMusicThumbLoader::GetEmbeddedThumb(const std::string &path, EmbeddedArt &art) +{ + CFileItem item(path, false); + std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(item)); + CMusicInfoTag tag; + if (nullptr != pLoader) + pLoader->Load(path, tag, &art); + + return !art.Empty(); +} diff --git a/xbmc/music/MusicThumbLoader.h b/xbmc/music/MusicThumbLoader.h new file mode 100644 index 0000000..fa02ad5 --- /dev/null +++ b/xbmc/music/MusicThumbLoader.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ThumbLoader.h" + +#include <map> + +class CFileItem; +class CMusicDatabase; +class EmbeddedArt; + +class CMusicThumbLoader : public CThumbLoader +{ +public: + CMusicThumbLoader(); + ~CMusicThumbLoader() override; + + void OnLoaderStart() override; + void OnLoaderFinish() override; + + bool LoadItem(CFileItem* pItem) override; + bool LoadItemCached(CFileItem* pItem) override; + bool LoadItemLookup(CFileItem* pItem) override; + + /*! \brief Helper function to fill all the art for a music library item + This fetches the original url for each type of art, and sets fallback thumb and fanart. + For songs the art for the related album and artist(s) is also set, and for albums that + of the related artist(s). Art type is named according to media type of the item, + for example: + artists may have "thumb", "fanart", "logo", "poster" etc., + albums may have "thumb", "spine" etc. and "artist.thumb", "artist.fanart" etc., + songs may have "thumb", "album.thumb", "artist.thumb", "artist.fanart", "artist.logo",... + "artist1.thumb", "artist1.fanart",... "albumartist.thumb", "albumartist1.thumb" etc. + \param item a music CFileItem + \return true if we fill art, false if there is no art found + */ + bool FillLibraryArt(CFileItem &item) override; + + /*! \brief Fill the thumb of a music file/folder item + First uses a cached thumb from a previous run, then checks for a local thumb + and caches it for the next run + \param item the CFileItem object to fill + \return true if we fill the thumb, false otherwise + */ + virtual bool FillThumb(CFileItem &item, bool folderThumbs = true); + + static bool GetEmbeddedThumb(const std::string &path, EmbeddedArt &art); + +protected: + CMusicDatabase *m_musicDatabase; + typedef std::map<int, std::map<std::string, std::string> > ArtCache; + ArtCache m_albumArt; +}; diff --git a/xbmc/music/MusicUtils.cpp b/xbmc/music/MusicUtils.cpp new file mode 100644 index 0000000..ac66aa3 --- /dev/null +++ b/xbmc/music/MusicUtils.cpp @@ -0,0 +1,841 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicUtils.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "PartyModeManager.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogSelect.h" +#include "filesystem/Directory.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "media/MediaType.h" +#include "music/MusicDatabase.h" +#include "music/MusicDbUrl.h" +#include "music/tags/MusicInfoTag.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "profiles/ProfileManager.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/IRunnable.h" +#include "utils/FileUtils.h" +#include "utils/JobManager.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "view/GUIViewState.h" + +using namespace MUSIC_INFO; +using namespace XFILE; +using namespace std::chrono_literals; + +namespace MUSIC_UTILS +{ +class CSetArtJob : public CJob +{ + CFileItemPtr pItem; + std::string m_artType; + std::string m_newArt; + +public: + CSetArtJob(const CFileItemPtr& item, const std::string& type, const std::string& newArt) + : pItem(item), m_artType(type), m_newArt(newArt) + { + } + + ~CSetArtJob(void) override = default; + + bool HasSongExtraArtChanged(const CFileItemPtr& pSongItem, + const std::string& type, + const int itemID, + CMusicDatabase& db) + { + if (!pSongItem->HasMusicInfoTag()) + return false; + int idSong = pSongItem->GetMusicInfoTag()->GetDatabaseId(); + if (idSong <= 0) + return false; + bool result = false; + if (type == MediaTypeAlbum) + // Update art when song is from album + result = (itemID == pSongItem->GetMusicInfoTag()->GetAlbumId()); + else if (type == MediaTypeArtist) + { + // Update art when artist is song or album artist of the song + if (pSongItem->HasProperty("artistid")) + { + // Check artistid property when we have it + for (CVariant::const_iterator_array varid = + pSongItem->GetProperty("artistid").begin_array(); + varid != pSongItem->GetProperty("artistid").end_array(); ++varid) + { + int idArtist = static_cast<int>(varid->asInteger()); + result = (itemID == idArtist); + if (result) + break; + } + } + else + { // Check song artists in database + result = db.IsSongArtist(idSong, itemID); + } + if (!result) + { + // Check song album artists + result = db.IsSongAlbumArtist(idSong, itemID); + } + } + return result; + } + + // Asynchronously update song, album or artist art in library + // and trigger update to album & artist art of the currently playing song + // and songs queued in the current playlist + bool DoWork(void) override + { + int itemID = pItem->GetMusicInfoTag()->GetDatabaseId(); + if (itemID <= 0) + return false; + std::string type = pItem->GetMusicInfoTag()->GetType(); + CMusicDatabase db; + if (!db.Open()) + return false; + if (!m_newArt.empty()) + db.SetArtForItem(itemID, type, m_artType, m_newArt); + else + db.RemoveArtForItem(itemID, type, m_artType); + // Artwork changed so set datemodified field for artist, album or song + db.SetItemUpdated(itemID, type); + + /* Update the art of the songs of the current music playlist. + Song thumb is often a fallback from the album and fanart is from the artist(s). + Clear the art if it is a song from the album or by the artist + (as song or album artist) that has modified artwork. The new artwork gets + loaded when the playlist is shown. + */ + bool clearcache(false); + const PLAYLIST::CPlayList& playlist = + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC); + + for (int i = 0; i < playlist.size(); ++i) + { + CFileItemPtr songitem = playlist[i]; + if (HasSongExtraArtChanged(songitem, type, itemID, db)) + { + songitem->ClearArt(); // Art gets reloaded when the current playist is shown + clearcache = true; + } + } + if (clearcache) + { + // Clear the music playlist from cache + CFileItemList items("playlistmusic://"); + items.RemoveDiscCache(WINDOW_MUSIC_PLAYLIST); + } + + // Similarly update the art of the currently playing song so it shows on OSD + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingAudio() && g_application.CurrentFileItem().HasMusicInfoTag()) + { + CFileItemPtr songitem = CFileItemPtr(new CFileItem(g_application.CurrentFileItem())); + if (HasSongExtraArtChanged(songitem, type, itemID, db)) + g_application.UpdateCurrentPlayArt(); + } + + db.Close(); + return true; + } +}; + +class CSetSongRatingJob : public CJob +{ + std::string strPath; + int idSong; + int iUserrating; + +public: + CSetSongRatingJob(const std::string& filePath, int userrating) + : strPath(filePath), idSong(-1), iUserrating(userrating) + { + } + + CSetSongRatingJob(int songId, int userrating) : strPath(), idSong(songId), iUserrating(userrating) + { + } + + ~CSetSongRatingJob(void) override = default; + + bool DoWork(void) override + { + // Asynchronously update song userrating in library + CMusicDatabase db; + if (db.Open()) + { + if (idSong > 0) + db.SetSongUserrating(idSong, iUserrating); + else + db.SetSongUserrating(strPath, iUserrating); + db.Close(); + } + + return true; + } +}; + +void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem, + const std::string& strType, + const std::string& strArt) +{ + // Asynchronously update that type of art in the database + CSetArtJob* job = new CSetArtJob(pItem, strType, strArt); + CServiceBroker::GetJobManager()->AddJob(job, nullptr); +} + +// Add art types required in Kodi and configured by the user +void AddHardCodedAndExtendedArtTypes(std::vector<std::string>& artTypes, const CMusicInfoTag& tag) +{ + for (const auto& artType : GetArtTypesToScan(tag.GetType())) + { + if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end()) + artTypes.push_back(artType); + } +} + +// Add art types currently assigned to the media item +void AddCurrentArtTypes(std::vector<std::string>& artTypes, + const CMusicInfoTag& tag, + CMusicDatabase& db) +{ + std::map<std::string, std::string> currentArt; + db.GetArtForItem(tag.GetDatabaseId(), tag.GetType(), currentArt); + for (const auto& art : currentArt) + { + if (!art.second.empty() && find(artTypes.begin(), artTypes.end(), art.first) == artTypes.end()) + artTypes.push_back(art.first); + } +} + +// Add art types that exist for other media items of the same type +void AddMediaTypeArtTypes(std::vector<std::string>& artTypes, + const CMusicInfoTag& tag, + CMusicDatabase& db) +{ + std::vector<std::string> dbArtTypes; + db.GetArtTypes(tag.GetType(), dbArtTypes); + for (const auto& artType : dbArtTypes) + { + if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end()) + artTypes.push_back(artType); + } +} + +// Add art types from available but unassigned artwork for this media item +void AddAvailableArtTypes(std::vector<std::string>& artTypes, + const CMusicInfoTag& tag, + CMusicDatabase& db) +{ + for (const auto& artType : db.GetAvailableArtTypesForItem(tag.GetDatabaseId(), tag.GetType())) + { + if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end()) + artTypes.push_back(artType); + } +} + +bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist) +{ + const CMusicInfoTag& tag = *musicitem.GetMusicInfoTag(); + if (tag.GetDatabaseId() < 1 || tag.GetType().empty()) + return false; + if (tag.GetType() != MediaTypeArtist && tag.GetType() != MediaTypeAlbum && + tag.GetType() != MediaTypeSong) + return false; + + artlist.Clear(); + + CMusicDatabase db; + db.Open(); + + std::vector<std::string> artTypes; + + AddHardCodedAndExtendedArtTypes(artTypes, tag); + AddCurrentArtTypes(artTypes, tag, db); + AddMediaTypeArtTypes(artTypes, tag, db); + AddAvailableArtTypes(artTypes, tag, db); + + db.Close(); + + for (const auto& type : artTypes) + { + CFileItemPtr artitem(new CFileItem(type, false)); + // Localise the names of common types of art + if (type == "banner") + artitem->SetLabel(g_localizeStrings.Get(20020)); + else if (type == "fanart") + artitem->SetLabel(g_localizeStrings.Get(20445)); + else if (type == "poster") + artitem->SetLabel(g_localizeStrings.Get(20021)); + else if (type == "thumb") + artitem->SetLabel(g_localizeStrings.Get(21371)); + else + artitem->SetLabel(type); + // Set art type as art item property + artitem->SetProperty("arttype", type); + // Set current art as art item thumb + if (musicitem.HasArt(type)) + artitem->SetArt("thumb", musicitem.GetArt(type)); + artlist.Add(artitem); + } + + return !artlist.IsEmpty(); +} + +std::string ShowSelectArtTypeDialog(CFileItemList& artitems) +{ + // Prompt for choice + CGUIDialogSelect* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!dialog) + return ""; + + dialog->SetHeading(CVariant{13521}); + dialog->Reset(); + dialog->SetUseDetails(true); + dialog->EnableButton(true, 13516); + + dialog->SetItems(artitems); + dialog->Open(); + + if (dialog->IsButtonPressed()) + { + // Get the new art type name + std::string strArtTypeName; + if (!CGUIKeyboardFactory::ShowAndGetInput(strArtTypeName, + CVariant{g_localizeStrings.Get(13516)}, false)) + return ""; + // Add new type to the list of art types + CFileItemPtr artitem(new CFileItem(strArtTypeName, false)); + artitem->SetLabel(strArtTypeName); + artitem->SetProperty("arttype", strArtTypeName); + artitems.Add(artitem); + + return strArtTypeName; + } + + return dialog->GetSelectedFileItem()->GetProperty("arttype").asString(); +} + +int ShowSelectRatingDialog(int iSelected) +{ + CGUIDialogSelect* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (dialog) + { + dialog->SetHeading(CVariant{38023}); + dialog->Add(g_localizeStrings.Get(38022)); + for (int i = 1; i <= 10; i++) + dialog->Add(StringUtils::Format("{}: {}", g_localizeStrings.Get(563), i)); + dialog->SetSelected(iSelected); + dialog->Open(); + + int userrating = dialog->GetSelectedItem(); + userrating = std::max(userrating, -1); + userrating = std::min(userrating, 10); + return userrating; + } + return -1; +} + +void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating) +{ + // Asynchronously update the song user rating in music library + const CMusicInfoTag* tag = pItem->GetMusicInfoTag(); + CSetSongRatingJob* job; + if (tag && tag->GetType() == MediaTypeSong && tag->GetDatabaseId() > 0) + // Use song ID when known + job = new CSetSongRatingJob(tag->GetDatabaseId(), userrating); + else + job = new CSetSongRatingJob(pItem->GetPath(), userrating); + CServiceBroker::GetJobManager()->AddJob(job, nullptr); +} + +std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType) +{ + std::vector<std::string> arttypes; + // Get default types of art that are to be automatically fetched during scanning + if (mediaType == MediaTypeArtist) + { + arttypes = {"thumb", "fanart"}; + for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST)) + { + if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end()) + arttypes.emplace_back(artType.asString()); + } + } + else if (mediaType == MediaTypeAlbum) + { + arttypes = {"thumb"}; + for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST)) + { + if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end()) + arttypes.emplace_back(artType.asString()); + } + } + return arttypes; +} + +bool IsValidArtType(const std::string& potentialArtType) +{ + // Check length and is ascii + return potentialArtType.length() <= 25 && + std::find_if_not(potentialArtType.begin(), potentialArtType.end(), + StringUtils::isasciialphanum) == potentialArtType.end(); +} + +} // namespace MUSIC_UTILS + +namespace +{ +class CAsyncGetItemsForPlaylist : public IRunnable +{ +public: + CAsyncGetItemsForPlaylist(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems) + : m_item(item), m_queuedItems(queuedItems) + { + } + + ~CAsyncGetItemsForPlaylist() override = default; + + void Run() override + { + // fast lookup is needed here + m_queuedItems.SetFastLookup(true); + + m_musicDatabase.Open(); + GetItemsForPlaylist(m_item); + m_musicDatabase.Close(); + } + +private: + void GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item); + + const std::shared_ptr<CFileItem> m_item; + CFileItemList& m_queuedItems; + CMusicDatabase m_musicDatabase; +}; + +SortDescription GetSortDescription(const CGUIViewState& state, const CFileItemList& items) +{ + SortDescription sortDescTrackNumber; + + auto sortDescriptions = state.GetSortDescriptions(); + for (auto& sortDescription : sortDescriptions) + { + if (sortDescription.sortBy == SortByTrackNumber) + { + // check whether at least one item has actually a track number set + for (const auto& item : items) + { + if (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetTrackNumber() > 0) + { + // First choice for folders containing a single album + sortDescTrackNumber = sortDescription; + sortDescTrackNumber.sortOrder = SortOrderAscending; + break; // leave items loop. we can still find ByArtistThenYear. so, no return here. + } + } + } + else if (sortDescription.sortBy == SortByArtistThenYear) + { + // check whether songs from at least two different albums are in the list + int lastAlbumId = -1; + for (const auto& item : items) + { + if (item->HasMusicInfoTag()) + { + const auto tag = item->GetMusicInfoTag(); + if (lastAlbumId != -1 && tag->GetAlbumId() != lastAlbumId) + { + // First choice for folders containing multiple albums + sortDescription.sortOrder = SortOrderAscending; + return sortDescription; + } + lastAlbumId = tag->GetAlbumId(); + } + } + } + } + + if (sortDescTrackNumber.sortBy != SortByNone) + return sortDescTrackNumber; + else + return state.GetSortMethod(); // last resort +} + +void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item) +{ + if (item->IsParentFolder() || !item->CanQueue() || item->IsRAR() || item->IsZIP()) + return; + + if (item->IsMusicDb() && item->m_bIsFolder && !item->IsParentFolder()) + { + // we have a music database folder, just grab the "all" item underneath it + XFILE::CMusicDatabaseDirectory dir; + + if (!dir.ContainsSongs(item->GetPath())) + { + // grab the ALL item in this category + // Genres will still require 2 lookups, and queuing the entire Genre folder + // will require 3 lookups (genre, artist, album) + CMusicDbUrl musicUrl; + if (musicUrl.FromString(item->GetPath())) + { + musicUrl.AppendPath("-1/"); + + const auto allItem = std::make_shared<CFileItem>(musicUrl.ToString(), true); + allItem->SetCanQueue(true); // workaround for CanQueue() check above + GetItemsForPlaylist(allItem); + } + return; + } + } + + if (item->m_bIsFolder) + { + // Check if we add a locked share + if (item->m_bIsShareOrDrive) + { + if (!g_passwordManager.IsItemUnlocked(item.get(), "music")) + return; + } + + CFileItemList items; + XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS); + + const std::unique_ptr<CGUIViewState> state( + CGUIViewState::GetViewState(WINDOW_MUSIC_NAV, items)); + if (state) + { + LABEL_MASKS labelMasks; + state->GetSortMethodLabelMasks(labelMasks); + + const CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File); + const CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder, + labelMasks.m_strLabel2Folder); + for (const auto& i : items) + { + if (i->IsLabelPreformatted()) + continue; + + if (i->m_bIsFolder) + folderFormatter.FormatLabels(i.get()); + else + fileFormatter.FormatLabels(i.get()); + } + + SortDescription sortDesc; + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV) + sortDesc = state->GetSortMethod(); + else + sortDesc = GetSortDescription(*state, items); + + if (sortDesc.sortBy == SortByLabel) + items.ClearSortState(); + + items.Sort(sortDesc); + } + + for (const auto& i : items) + { + GetItemsForPlaylist(i); + } + } + else + { + if (item->IsPlayList()) + { + const std::unique_ptr<PLAYLIST::CPlayList> playList( + PLAYLIST::CPlayListFactory::Create(*item)); + if (!playList) + { + CLog::Log(LOGERROR, "{} failed to create playlist {}", __FUNCTION__, item->GetPath()); + return; + } + + if (!playList->Load(item->GetPath())) + { + CLog::Log(LOGERROR, "{} failed to load playlist {}", __FUNCTION__, item->GetPath()); + return; + } + + for (int i = 0; i < playList->size(); ++i) + { + GetItemsForPlaylist((*playList)[i]); + } + } + else if (item->IsInternetStream() && !item->IsMusicDb()) + { + // just queue the internet stream, it will be expanded on play + m_queuedItems.Add(item); + } + else if (item->IsPlugin() && item->GetProperty("isplayable").asBoolean()) + { + // python files can be played + m_queuedItems.Add(item); + } + else if (!item->IsNFO() && (item->IsAudio() || item->IsVideo())) + { + const auto itemCheck = m_queuedItems.Get(item->GetPath()); + if (!itemCheck || itemCheck->GetStartOffset() != item->GetStartOffset()) + { + // add item + m_musicDatabase.SetPropertiesForFileItem(*item); + m_queuedItems.Add(item); + } + } + } +} + +void ShowToastNotification(const CFileItem& item, int titleId) +{ + std::string localizedMediaType; + std::string title; + + if (item.HasMusicInfoTag()) + { + localizedMediaType = CMediaTypes::GetCapitalLocalization(item.GetMusicInfoTag()->GetType()); + title = item.GetMusicInfoTag()->GetTitle(); + } + + if (title.empty()) + title = item.GetLabel(); + if (title.empty()) + return; // no meaningful toast possible. + + const std::string message = + localizedMediaType.empty() ? title : localizedMediaType + ": " + title; + + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(titleId), + message); +} +} // unnamed namespace + +namespace MUSIC_UTILS +{ +void PlayItem(const std::shared_ptr<CFileItem>& itemIn) +{ + auto item = itemIn; + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!itemIn->CanQueue()) + { + // make a copy to not alter the original item + item = std::make_shared<CFileItem>(*itemIn); + item->SetCanQueue(true); + } + + if (item->m_bIsFolder) + { + // build a playlist and play it + CFileItemList queuedItems; + GetItemsForPlayList(item, queuedItems); + + auto& player = CServiceBroker::GetPlaylistPlayer(); + player.ClearPlaylist(PLAYLIST::TYPE_MUSIC); + player.Reset(); + player.Add(PLAYLIST::TYPE_MUSIC, queuedItems); + player.SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC); + player.Play(); + } + else if (item->HasMusicInfoTag()) + { + // song, so just play it + CServiceBroker::GetPlaylistPlayer().Play(item, ""); + } +} + +void QueueItem(const std::shared_ptr<CFileItem>& itemIn, QueuePosition pos) +{ + auto item = itemIn; + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!itemIn->CanQueue()) + { + // make a copy to not alter the original item + item = std::make_shared<CFileItem>(*itemIn); + item->SetCanQueue(true); + } + + auto& player = CServiceBroker::GetPlaylistPlayer(); + + PLAYLIST::Id playlistId = player.GetCurrentPlaylist(); + if (playlistId == PLAYLIST::TYPE_NONE) + { + const auto& components = CServiceBroker::GetAppComponents(); + playlistId = components.GetComponent<CApplicationPlayer>()->GetPreferredPlaylist(); + } + + if (playlistId == PLAYLIST::TYPE_NONE) + playlistId = PLAYLIST::TYPE_MUSIC; + + // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exists + if (item->IsSmartPlayList() && !CFileUtils::Exists(item->GetPath())) + { + const auto profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + if (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) + return; + } + + const int oldSize = player.GetPlaylist(playlistId).size(); + + CFileItemList queuedItems; + GetItemsForPlayList(item, queuedItems); + + // if party mode, add items but DONT start playing + if (g_partyModeManager.IsEnabled()) + { + g_partyModeManager.AddUserSongs(queuedItems, false); + return; + } + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + if (pos == QueuePosition::POSITION_BEGIN && appPlayer->IsPlaying()) + player.Insert(playlistId, queuedItems, + CServiceBroker::GetPlaylistPlayer().GetCurrentSong() + 1); + else + player.Add(playlistId, queuedItems); + + bool playbackStarted = false; + + if (!appPlayer->IsPlaying() && player.GetPlaylist(playlistId).size()) + { + const int winID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + if (winID == WINDOW_MUSIC_NAV) + { + CGUIViewState* viewState = CGUIViewState::GetViewState(winID, queuedItems); + if (viewState) + viewState->SetPlaylistDirectory("playlistmusic://"); + } + + player.Reset(); + player.SetCurrentPlaylist(playlistId); + player.Play(oldSize, ""); // start playing at the first new item + + playbackStarted = true; + } + + if (!playbackStarted) + { + if (pos == QueuePosition::POSITION_END) + ShowToastNotification(*item, 38082); // Added to end of playlist + else + ShowToastNotification(*item, 38083); // Added to playlist to play next + } +} + +bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems) +{ + CAsyncGetItemsForPlaylist getItems(item, queuedItems); + return CGUIDialogBusy::Wait(&getItems, + 500, // 500ms before busy dialog appears + true); // can be cancelled +} + +bool IsItemPlayable(const CFileItem& item) +{ + // Exclude all parent folders + if (item.IsParentFolder()) + return false; + + // Exclude all video library items + if (item.IsVideoDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://video/")) + return false; + + // Exclude other components + if (item.IsPVR() || item.IsPlugin() || item.IsScript() || item.IsAddonsPath()) + return false; + + // Exclude special items + if (StringUtils::StartsWithNoCase(item.GetPath(), "newsmartplaylist://") || + StringUtils::StartsWithNoCase(item.GetPath(), "newplaylist://")) + return false; + + // Exclude unwanted windows + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_PLAYLIST) + return false; + + // Include playlists located at one of the possible music playlist locations + if (item.IsPlayList()) + { + if (StringUtils::StartsWithNoCase(item.GetMimeType(), "audio/")) + return true; + + if (StringUtils::StartsWithNoCase(item.GetPath(), "special://musicplaylists/") || + StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/music/")) + return true; + + // Has user changed default playlists location and the list is located there? + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + std::string path = settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH); + StringUtils::TrimRight(path, "/"); + if (StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/music/", path))) + return true; + + if (!item.m_bIsFolder) + { + // Unknown location. Type cannot be determined for non-folder items. + return false; + } + } + + if (item.m_bIsFolder && + (item.IsMusicDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://music/"))) + { + // Exclude top level nodes - eg can't play 'genres' just a specific genre etc + const XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE node = + XFILE::CMusicDatabaseDirectory::GetDirectoryParentType(item.GetPath()); + if (node == XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE_OVERVIEW) + return false; + + return true; + } + + if (item.HasMusicInfoTag() && item.CanQueue()) + return true; + else if (!item.m_bIsFolder && item.IsAudio()) + return true; + else if (item.m_bIsFolder) + { + // Not a music-specific folder (just file:// or nfs://). Allow play if context is Music window. + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV && + item.GetPath() != "add") // Exclude "Add music source" item + return true; + } + return false; +} + +} // namespace MUSIC_UTILS diff --git a/xbmc/music/MusicUtils.h b/xbmc/music/MusicUtils.h new file mode 100644 index 0000000..c9b6f94 --- /dev/null +++ b/xbmc/music/MusicUtils.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "media/MediaType.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; +class CFileItemList; + +namespace MUSIC_UTILS +{ +/*! \brief Show a dialog to allow the selection of type of art from a list. + Input is a fileitem list, with each item having an "arttype" property + e.g. "thumb", current art URL (if art exists), and label. One of these art types + can be selected, or a new art type added. The new art type is added as a new item + in the list, as well as returned as the selected art type. + \param artitems [in/out] a fileitem list to display + \return the selected art type e.g. "fanart" or empty string when cancelled. + \sa FillArtTypesList + */ +std::string ShowSelectArtTypeDialog(CFileItemList& artitems); + +/*! \brief Helper function to build a list of art types for a music library item. + This fetches the possible types of art for a song, album or artist, and the + current art URL (if the item has art of that type), for display on a dialog. + \param musicitem a music CFileItem (song, album or artist) + \param artitems [out] a fileitem list, each item having "arttype" property + e.g. "thumb", current art URL (if art exists), and localized label (for common arttypes) + \return true if art types are retrieved, false if none is found. + \sa ShowSelectArtTypeDialog + */ +bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist); + +/*! \brief Helper function to asynchronously update art in the music database + and then refresh the album & artist art of the currently playing song. + For the song, album or artist this adds a job to the queue to update the art table + modifying, adding or deleting that type of art. Changes to album or artist art are + then passed to the currently playing song (if there is one). + \param item a shared pointer to a music CFileItem (song, album or artist) + \param strType the type of art e.g. "fanart" or "thumb" etc. + \param strArt art URL, when empty the entry for that type of art is deleted. + */ +void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem, + const std::string& strType, + const std::string& strArt); + +/*! \brief Show a dialog to allow the selection of user rating. + \param iSelected the rating to show initially + \return the selected rating, 0 (no rating), 1 to 10 or -1 no rating selected + */ +int ShowSelectRatingDialog(int iSelected); + +/*! \brief Helper function to asynchronously update the user rating of a song + \param pItem pointer to song item being rated + \param userrating the userrating 0 = no rating, 1 to 10 + */ +void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating); + +/*! \brief Get the types of art for an artist or album that are to be + automatically fetched from local files during scanning + \param mediaType [in] artist or album + \return vector of art types that are to be fetched during scanning + */ +std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType); + +/*! \brief Validate string is acceptable as the name of an additional art type + - limited length, and ascii alphanumberic characters only + \param potentialArtType [in] potential art type name + \return true if the art type is valid + */ +bool IsValidArtType(const std::string& potentialArtType); + +/*! \brief Start playback of the given item. If the item is a folder, build a playlist with + all items contained in the folder and start playback of the playlist. If item is a single music + item, start playback directly, without adding it to the music playlist first. + \param item [in] the item to play + */ +void PlayItem(const std::shared_ptr<CFileItem>& item); + +enum class QueuePosition +{ + POSITION_BEGIN, // place at begin of queue, before other items + POSITION_END, // place at end of queue, after other items +}; + +/*! \brief Queue the given item in the currently active playlist. If none is active, put the + item into the music playlist. Start playback of the playlist, if player is not already playing. + \param item [in] the item to queue + \param pos [in] whether to place the item and the begin or the end of the queue + */ +void QueueItem(const std::shared_ptr<CFileItem>& item, QueuePosition pos); + +/*! \brief For a given item, get the items to put in a playlist. If the item is a folder, all + subitems will be added recursively to the returned item list. If the item is a playlist, the + playlist will be loaded and contained items will be added to the returned item list. Shows a + busy dialog if action takes certain amount of time to give the user visual feedback. + \param item [in] the item to add to the playlist + \param queuedItems [out] the items that can be put in a play list + \return true on success, false otherwise + */ +bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems); + +/*! + \brief Check whether the given item can be played by the app playlist player as one or more songs. + \param item The item to check + \return True if playable, false otherwise. + */ +bool IsItemPlayable(const CFileItem& item); + +} // namespace MUSIC_UTILS diff --git a/xbmc/music/Song.cpp b/xbmc/music/Song.cpp new file mode 100644 index 0000000..165889c --- /dev/null +++ b/xbmc/music/Song.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Song.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace MUSIC_INFO; + +CSong::CSong(CFileItem& item) +{ + CMusicInfoTag& tag = *item.GetMusicInfoTag(); + strTitle = tag.GetTitle(); + genre = tag.GetGenre(); + strArtistDesc = tag.GetArtistString(); + //Set sort string before processing artist credits + strArtistSort = tag.GetArtistSort(); + m_strComposerSort = tag.GetComposerSort(); + + // Determine artist credits from various tag arrays + SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID()); + + strAlbum = tag.GetAlbum(); + m_albumArtist = tag.GetAlbumArtist(); + // Separate album artist names further, if possible, and trim blank space. + if (tag.GetMusicBrainzAlbumArtistHints().size() > m_albumArtist.size()) + // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names + m_albumArtist = tag.GetMusicBrainzAlbumArtistHints(); + else + // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader + m_albumArtist = StringUtils::SplitMulti(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators); + for (auto artistname : m_albumArtist) + StringUtils::Trim(artistname); + m_strAlbumArtistSort = tag.GetAlbumArtistSort(); + + strMusicBrainzTrackID = tag.GetMusicBrainzTrackID(); + m_musicRoles = tag.GetContributors(); + strComment = tag.GetComment(); + strCueSheet = tag.GetCueSheet(); + strMood = tag.GetMood(); + rating = tag.GetRating(); + userrating = tag.GetUserrating(); + votes = tag.GetVotes(); + strOrigReleaseDate = tag.GetOriginalDate(); + strReleaseDate = tag.GetReleaseDate(); + strDiscSubtitle = tag.GetDiscSubtitle(); + iTrack = tag.GetTrackAndDiscNumber(); + iDuration = tag.GetDuration(); + strRecordLabel = tag.GetRecordLabel(); + strAlbumType = tag.GetMusicBrainzReleaseType(); + bCompilation = tag.GetCompilation(); + embeddedArt = tag.GetCoverArtInfo(); + strFileName = tag.GetURL().empty() ? item.GetPath() : tag.GetURL(); + dateAdded = tag.GetDateAdded(); + replayGain = tag.GetReplayGain(); + strThumb = item.GetUserMusicThumb(true); + iStartOffset = static_cast<int>(item.GetStartOffset()); + iEndOffset = static_cast<int>(item.GetEndOffset()); + idSong = -1; + iTimesPlayed = 0; + idAlbum = -1; + iBPM = tag.GetBPM(); + iSampleRate = tag.GetSampleRate(); + iBitRate = tag.GetBitRate(); + iChannels = tag.GetNoOfChannels(); +} + +CSong::CSong() +{ + Clear(); +} + +void CSong::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints, + const std::vector<std::string>& mbids) +{ + artistCredits.clear(); + std::vector<std::string> artistHints = hints; + //Split the artist sort string to try and get sort names for individual artists + std::vector<std::string> artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + + if (!mbids.empty()) + { // Have musicbrainz artist info, so use it + + // Vector of possible separators in the order least likely to be part of artist name + const std::vector<std::string> separators{ " feat. ", " ft. ", " Feat. "," Ft. ", ";", ":", "|", "#", "/", " with ", ",", "&" }; + + // Establish tag consistency - do the number of musicbrainz ids and number of names in hints or artist match + if (mbids.size() != artistHints.size() && mbids.size() != names.size()) + { + // Tags mismatch - report it and then try to fix + CLog::Log(LOGDEBUG, "Mismatch in song file tags: {} mbid {} names {} {}", (int)mbids.size(), + (int)names.size(), strTitle, strArtistDesc); + /* + Most likely we have no hints and a single artist name like "Artist1 feat. Artist2" + or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the + expected single item separator (default = space-slash-space) as not been used. + Ampersand (&), comma and slash (no spaces) are poor delimiters as could be in name + e.g. "AC/DC", "Earth, Wind & Fire", but here treat them as such in attempt to find artist names. + When there are hints but count not match mbid they could be poorly formatted using unexpected + separators so attempt to split them. Or we could have more hints or artist names than + musicbrainz id so ignore them but raise warning. + */ + // Do hints exist yet mismatch + if (artistHints.size() > 0 && + artistHints.size() != mbids.size()) + { + if (names.size() == mbids.size()) + // Artist name count matches, use that as hints + artistHints = names; + else if (artistHints.size() < mbids.size()) + { // Try splitting the hints until have matching number + artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size()); + } + else + // Extra hints, discard them. + artistHints.resize(mbids.size()); + } + // Do hints not exist or still mismatch, try artists + if (artistHints.size() != mbids.size()) + artistHints = names; + // Still mismatch, try splitting the hints (now artists) until have matching number + if (artistHints.size() < mbids.size()) + { + artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size()); + } + } + else + { // Either hints or artist names (or both) matches number of musicbrainz id + // If hints mismatch, use artists + if (artistHints.size() != mbids.size()) + artistHints = names; + } + + // Try to get number of artist sort names and musicbrainz ids to match. Split sort names + // further using multiple possible delimiters, over single separator applied in Tag loader + if (artistSort.size() != mbids.size()) + artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" }); + + for (size_t i = 0; i < mbids.size(); i++) + { + std::string artistId = mbids[i]; + std::string artistName; + /* + We try and get the corresponding artist name from the hints list. + Having already attempted to make the number of hints match, if they + still don't then use musicbrainz id as the name and hope later on we + can update that entry. + */ + if (i < artistHints.size()) + artistName = artistHints[i]; + else + artistName = artistId; + + // Use artist sort name providing we have as many as we have mbid, + // otherwise something is wrong with them so ignore and leave blank + if (artistSort.size() == mbids.size()) + artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId); + else + artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId); + } + } + else + { // No musicbrainz artist ids, so fill in directly + // Separate artist names further, if possible, and trim blank space. + std::vector<std::string> artists = names; + if (artistHints.size() > names.size()) + // Make use of hints (ARTISTS tag), when present, to separate artist names + artists = artistHints; + else + // Split artist names further using multiple possible delimiters, over single separator applied in Tag loader + artists = StringUtils::SplitMulti(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators); + + if (artistSort.size() != artists.size()) + // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader + artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" }); + + for (size_t i = 0; i < artists.size(); i++) + { + artistCredits.emplace_back(StringUtils::Trim(artists[i])); + // Set artist sort name providing we have as many as we have artists, + // otherwise something is wrong with them so ignore rather than guess. + if (artistSort.size() == artists.size()) + artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i])); + } + } + +} + +void CSong::MergeScrapedSong(const CSong& source, bool override) +{ + // Merge when MusicBrainz Track ID match (checked in CAlbum::MergeScrapedAlbum) + if ((override && !source.strTitle.empty()) || strTitle.empty()) + strTitle = source.strTitle; + if ((override && source.iTrack != 0) || iTrack == 0) + iTrack = source.iTrack; + if (override) + { + artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper + strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 feat. artist2" when scraped + } +} + +void CSong::Serialize(CVariant& value) const +{ + value["filename"] = strFileName; + value["title"] = strTitle; + value["artist"] = GetArtist(); + value["artistsort"] = GetArtistSort(); // a string for the song not vector of values for each artist + value["album"] = strAlbum; + value["albumartist"] = GetAlbumArtist(); + value["genre"] = genre; + value["duration"] = iDuration; + value["track"] = iTrack; + value["year"] = atoi(strReleaseDate.c_str());; + value["musicbrainztrackid"] = strMusicBrainzTrackID; + value["comment"] = strComment; + value["mood"] = strMood; + value["rating"] = rating; + value["userrating"] = userrating; + value["votes"] = votes; + value["timesplayed"] = iTimesPlayed; + value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDateTime() : ""; + value["dateadded"] = dateAdded.IsValid() ? dateAdded.GetAsDBDateTime() : ""; + value["albumid"] = idAlbum; + value["albumreleasedate"] = strReleaseDate; + value["bpm"] = iBPM; + value["bitrate"] = iBitRate; + value["samplerate"] = iSampleRate; + value["channels"] = iChannels; +} + +void CSong::Clear() +{ + strFileName.clear(); + strTitle.clear(); + strAlbum.clear(); + strArtistSort.clear(); + strArtistDesc.clear(); + m_albumArtist.clear(); + m_strAlbumArtistSort.clear(); + genre.clear(); + strThumb.clear(); + strMusicBrainzTrackID.clear(); + m_musicRoles.clear(); + strComment.clear(); + strMood.clear(); + rating = 0; + userrating = 0; + votes = 0; + iTrack = 0; + iDuration = 0; + strOrigReleaseDate.clear(); + strReleaseDate.clear(); + strDiscSubtitle.clear(); + iStartOffset = 0; + iEndOffset = 0; + idSong = -1; + iTimesPlayed = 0; + lastPlayed.Reset(); + dateAdded.Reset(); + dateUpdated.Reset(); + dateNew.Reset(); + idAlbum = -1; + bCompilation = false; + embeddedArt.Clear(); + iBPM = 0; + iBitRate = 0; + iSampleRate = 0; + iChannels = 0; + + replayGain = ReplayGain(); +} +const std::vector<std::string> CSong::GetArtist() const +{ + //Get artist names as vector from artist credits + std::vector<std::string> songartists; + for (const auto& artistCredit : artistCredits) + { + songartists.push_back(artistCredit.GetArtist()); + } + //When artist credits have not been populated attempt to build an artist vector from the description string + //This is a temporary fix, in the longer term other areas should query the song_artist table and populate + //artist credits. Note that splitting the string may not give the same artists as held in the song_artist table + if (songartists.empty() && !strArtistDesc.empty()) + songartists = StringUtils::Split(strArtistDesc, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + return songartists; +} + +const std::string CSong::GetArtistSort() const +{ + //The stored artist sort name string takes precedence but a + //value could be created from individual sort names held in artistcredits + if (!strArtistSort.empty()) + return strArtistSort; + std::vector<std::string> artistvector; + for (const auto& artistcredit : artistCredits) + if (!artistcredit.GetSortName().empty()) + artistvector.emplace_back(artistcredit.GetSortName()); + std::string artistString; + if (!artistvector.empty()) + artistString = StringUtils::Join(artistvector, "; "); + return artistString; +} + +const std::vector<std::string> CSong::GetMusicBrainzArtistID() const +{ + //Get artist MusicBrainz IDs as vector from artist credits + std::vector<std::string> musicBrainzID; + for (const auto& artistCredit : artistCredits) + { + musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID()); + } + return musicBrainzID; +} + +const std::string CSong::GetArtistString() const +{ + //Artist description may be different from the artists in artistcredits (see ARTISTS tag processing) + //but is takes precedence as a string because artistcredits is not always filled during processing + if (!strArtistDesc.empty()) + return strArtistDesc; + std::vector<std::string> artistvector; + for (const auto& i : artistCredits) + artistvector.push_back(i.GetArtist()); + std::string artistString; + if (!artistvector.empty()) + artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + return artistString; +} + +const std::vector<int> CSong::GetArtistIDArray() const +{ + // Get song artist IDs for json rpc + std::vector<int> artistids; + for (const auto& artistCredit : artistCredits) + artistids.push_back(artistCredit.GetArtistId()); + return artistids; +} + +void CSong::AppendArtistRole(const CMusicRole& musicRole) +{ + m_musicRoles.push_back(musicRole); +} + +bool CSong::HasArt() const +{ + if (!strThumb.empty()) return true; + if (!embeddedArt.Empty()) return true; + return false; +} + +bool CSong::ArtMatches(const CSong &right) const +{ + return (right.strThumb == strThumb && + embeddedArt.Matches(right.embeddedArt)); +} + +const std::string CSong::GetDiscSubtitle() const +{ + return strDiscSubtitle; +} diff --git a/xbmc/music/Song.h b/xbmc/music/Song.h new file mode 100644 index 0000000..3fc127a --- /dev/null +++ b/xbmc/music/Song.h @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +/*! + \file Song.h +\brief +*/ + +#include "Artist.h" +#include "XBDateTime.h" +#include "music/tags/ReplayGain.h" +#include "utils/EmbeddedArt.h" +#include "utils/ISerializable.h" + +#include <map> +#include <string> +#include <vector> + +class CVariant; + +/*! + \ingroup music + \brief Class to store and read album information from CMusicDatabase + \sa CSong, CMusicDatabase + */ + +class CGenre +{ +public: + int idGenre; + std::string strGenre; +}; + +class CFileItem; + +/*! + \ingroup music + \brief Class to store and read song information from CMusicDatabase + \sa CAlbum, CMusicDatabase + */ +class CSong final : public ISerializable +{ +public: + CSong() ; + explicit CSong(CFileItem& item); + void Clear() ; + void MergeScrapedSong(const CSong& source, bool override); + void Serialize(CVariant& value) const override; + + bool operator<(const CSong &song) const + { + if (strFileName < song.strFileName) return true; + if (strFileName > song.strFileName) return false; + if (iTrack < song.iTrack) return true; + return false; + } + + /*! \brief Get artist names from the vector of artistcredits objects + \return artist names as a vector of strings + */ + const std::vector<std::string> GetArtist() const; + + /*! \brief Get artist sort name string + \return artist sort name as a single string + */ + const std::string GetArtistSort() const; + + /*! \brief Get artist MusicBrainz IDs from the vector of artistcredits objects + \return artist MusicBrainz IDs as a vector of strings + */ + const std::vector<std::string> GetMusicBrainzArtistID() const; + + /*! \brief Get artist names from the artist description string (if it exists) + or concatenated from the vector of artistcredits objects + \return artist names as a single string + */ + const std::string GetArtistString() const; + + /*! \brief Get song artist IDs (for json rpc) from the vector of artistcredits objects + \return album artist IDs as a vector of integers + */ + const std::vector<int> GetArtistIDArray() const; + + /*! \brief Get album artist names associated with song from tag data + Note for initial album processing only, normalised album artist data belongs to album + and is stored in album artist credits + \return album artist names as a vector of strings + */ + const std::vector<std::string> GetAlbumArtist() const { return m_albumArtist; } + + /*! \brief Get album artist sort name string + \return album artist sort name as a single string + */ + const std::string GetAlbumArtistSort() const { return m_strAlbumArtistSort; } + + /*! \brief Get disc subtitle string where one exists + \return disc subtitle as a single string + */ + const std::string GetDiscSubtitle() const; + + /*! \brief Get composer sort name string + \return composer sort name as a single string + */ + const std::string GetComposerSort() const { return m_strComposerSort; } + + /*! \brief Get the full list of artist names and the role each played for those + that contributed to the recording. Given in music file tags other than ARTIST + or ALBUMARTIST, e.g. COMPOSER or CONDUCTOR etc. + \return a vector of all contributing artist names and their roles + */ + const VECMUSICROLES& GetContributors() const { return m_musicRoles; } + //void AddArtistRole(const int &role, const std::string &artist); + void AppendArtistRole(const CMusicRole& musicRole); + + /*! \brief Set album artist vector. + Album artist is held local to song until album created for initial processing only. + Normalised album artist data belongs to album and is stored in album artist credits + \param album artist names as a vector of strings + */ + void SetAlbumArtist(const std::vector<std::string>& albumartists) { m_albumArtist = albumartists; } + + /*! \brief Whether this song has any artists in artist credits vector + Tests if artist credits has been populated yet, during processing there can be + artists in the artist description but not yet in the credits + */ + bool HasArtistCredits() const { return !artistCredits.empty(); } + + /*! \brief Whether this song has any artists in music roles (contributors) vector + Tests if contributors has been populated yet, there may be none. + */ + bool HasContributors() const { return !m_musicRoles.empty(); } + + /*! \brief whether this song has art associated with it + Tests both the strThumb and embeddedArt members. + */ + bool HasArt() const; + + /*! \brief whether the art from this song matches the art from another + Tests both the strThumb and embeddedArt members. + */ + bool ArtMatches(const CSong &right) const; + + /*! \brief Set artist credits using the arrays of tag values. + If strArtistSort (as from ARTISTSORT tag) is already set then individual + artist sort names are also processed. + \param names String vector of artist names (as from ARTIST tag) + \param hints String vector of artist name hints (as from ARTISTS tag) + \param mbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag) + */ + void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints, + const std::vector<std::string>& mbids); + + int idSong; + int idAlbum; + std::string strFileName; + std::string strTitle; + std::string strArtistSort; + std::string strArtistDesc; + VECARTISTCREDITS artistCredits; + std::string strAlbum; + std::vector<std::string> genre; + std::string strThumb; + EmbeddedArtInfo embeddedArt; + std::string strMusicBrainzTrackID; + std::string strComment; + std::string strMood; + std::string strCueSheet; + float rating; + int userrating; + int votes; + int iTrack; + int iDuration; + std::string strOrigReleaseDate; + std::string strReleaseDate; + std::string strDiscSubtitle; + int iTimesPlayed; + CDateTime lastPlayed; + CDateTime dateAdded; // File creation or modification time, or when tags (re-)scanned + CDateTime dateUpdated; // Time db record Last modified + CDateTime dateNew; // Time db record created + int iStartOffset; + int iEndOffset; + bool bCompilation; + int iBPM; + int iSampleRate; + int iBitRate; + int iChannels; + std::string strRecordLabel; // Record label from tag for album processing by CMusicInfoScanner::FileItemsToAlbums + std::string strAlbumType; // (Musicbrainz release type) album type from tag for album processing by CMusicInfoScanner::FileItemsToAlbums + + ReplayGain replayGain; +private: + std::vector<std::string> m_albumArtist; // Album artist from tag for album processing, no desc or MBID + std::string m_strAlbumArtistSort; // Albumartist sort string from tag for album processing by CMusicInfoScanner::FileItemsToAlbums + std::string m_strComposerSort; + VECMUSICROLES m_musicRoles; +}; + +/*! + \ingroup music + \brief A vector of CSong objects, used for CMusicDatabase + \sa CMusicDatabase + */ +typedef std::vector<CSong> VECSONGS; + +/*! + \ingroup music + \brief A map of a vector of CSong objects key by filename, used for CMusicDatabase + */ +typedef std::map<std::string, VECSONGS> MAPSONGS; + +/*! + \ingroup music + \brief A vector of std::string objects, used for CMusicDatabase + \sa CMusicDatabase + */ +typedef std::vector<CGenre> VECGENRES; diff --git a/xbmc/music/dialogs/CMakeLists.txt b/xbmc/music/dialogs/CMakeLists.txt new file mode 100644 index 0000000..40b6e90 --- /dev/null +++ b/xbmc/music/dialogs/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES GUIDialogInfoProviderSettings.cpp + GUIDialogMusicInfo.cpp + GUIDialogMusicOSD.cpp + GUIDialogSongInfo.cpp + GUIDialogVisualisationPresetList.cpp) + +set(HEADERS GUIDialogInfoProviderSettings.h + GUIDialogMusicInfo.h + GUIDialogMusicOSD.h + GUIDialogSongInfo.h + GUIDialogVisualisationPresetList.h) + +core_add_library(music_dialogs) diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp new file mode 100644 index 0000000..a9e2232 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogInfoProviderSettings.h" + +#include "ServiceBroker.h" +#include "Util.h" +#include "addons/AddonManager.h" +#include "addons/AddonSystemSettings.h" +#include "addons/addoninfo/AddonType.h" +#include "addons/gui/GUIDialogAddonSettings.h" +#include "addons/gui/GUIWindowAddonBrowser.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "filesystem/AddonsDirectory.h" +#include "filesystem/Directory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "interfaces/builtins/Builtins.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/windows/GUIControlSettings.h" +#include "storage/MediaManager.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <limits.h> +#include <map> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +using namespace ADDON; + +const std::string SETTING_ALBUMSCRAPER_SETTINGS = "albumscrapersettings"; +const std::string SETTING_ARTISTSCRAPER_SETTINGS = "artistscrapersettings"; +const std::string SETTING_APPLYTOITEMS = "applysettingstoitems"; + +CGUIDialogInfoProviderSettings::CGUIDialogInfoProviderSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_INFOPROVIDER_SETTINGS, "DialogSettings.xml") +{ } + +bool CGUIDialogInfoProviderSettings::Show() +{ + CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS); + if (!dialog) + return false; + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + dialog->m_showSingleScraper = false; + + // Get current default info provider settings from service broker + dialog->m_fetchInfo = settings->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO); + + ADDON::AddonPtr defaultScraper; + // Get default album scraper (when enabled - can default scraper be disabled??) + if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS, + defaultScraper)) + { + ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper); + dialog->SetAlbumScraper(scraper); + } + + // Get default artist scraper + if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS, + defaultScraper)) + { + ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper); + dialog->SetArtistScraper(scraper); + } + + dialog->m_strArtistInfoPath = settings->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + + dialog->Open(); + + dialog->ResetDefaults(); + return dialog->IsConfirmed(); +} + +int CGUIDialogInfoProviderSettings::Show(ADDON::ScraperPtr& scraper) +{ + CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS); + if (!dialog || !scraper) + return -1; + if (scraper->Content() != CONTENT_ARTISTS && scraper->Content() != CONTENT_ALBUMS) + return -1; + + dialog->m_showSingleScraper = true; + dialog->m_singleScraperType = scraper->Content(); + + if (dialog->m_singleScraperType == CONTENT_ALBUMS) + dialog->SetAlbumScraper(scraper); + else + dialog->SetArtistScraper(scraper); + // toast selected but disabled scrapers + if (CServiceBroker::GetAddonMgr().IsAddonDisabled(scraper->ID())) + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(24024), scraper->Name(), 2000, true); + + dialog->Open(); + + bool confirmed = dialog->IsConfirmed(); + unsigned int applyToItems = dialog->m_applyToItems; + if (confirmed) + { + if (dialog->m_singleScraperType == CONTENT_ALBUMS) + scraper = dialog->GetAlbumScraper(); + else + { + scraper = dialog->GetArtistScraper(); + // Save artist information folder (here not in the caller) when applying setting as default for all artists + if (applyToItems == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, dialog->m_strArtistInfoPath); + } + if (scraper) + scraper->SetPathSettings(dialog->m_singleScraperType, ""); + } + + dialog->ResetDefaults(); + + if (confirmed) + return applyToItems; + else + return -1; +} + +void CGUIDialogInfoProviderSettings::OnInitWindow() +{ + CGUIDialogSettingsManualBase::OnInitWindow(); +} + +void CGUIDialogInfoProviderSettings::OnSettingChanged( + const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string &settingId = setting->GetId(); + + if (settingId == CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO) + { + m_fetchInfo = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + SetupView(); + SetFocus(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO); + } + else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER) + m_strArtistInfoPath = std::static_pointer_cast<const CSettingString>(setting)->GetValue(); + else if (settingId == SETTING_APPLYTOITEMS) + { + m_applyToItems = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + SetupView(); + SetFocus(SETTING_APPLYTOITEMS); + } +} + +void CGUIDialogInfoProviderSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + CGUIDialogSettingsManualBase::OnSettingAction(setting); + + const std::string &settingId = setting->GetId(); + + if (settingId == CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER) + { + std::string currentScraperId; + if (m_albumscraper) + currentScraperId = m_albumscraper->ID(); + std::string selectedAddonId = currentScraperId; + + if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ALBUMS, selectedAddonId, false) == + 1 && + selectedAddonId != currentScraperId) + { + AddonPtr scraperAddon; + if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon, + OnlyEnabled::CHOICE_YES)) + { + m_albumscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon); + SetupView(); + SetFocus(settingId); + } + else + { + CLog::Log(LOGERROR, "{} - Could not get settings for addon: {}", __FUNCTION__, + selectedAddonId); + } + } + } + else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER) + { + std::string currentScraperId; + if (m_artistscraper) + currentScraperId = m_artistscraper->ID(); + std::string selectedAddonId = currentScraperId; + + if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ARTISTS, selectedAddonId, false) == + 1 && + selectedAddonId != currentScraperId) + { + AddonPtr scraperAddon; + if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon, + OnlyEnabled::CHOICE_YES)) + { + m_artistscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon); + SetupView(); + SetFocus(settingId); + } + else + { + CLog::Log(LOGERROR, "{} - Could not get addon: {}", __FUNCTION__, selectedAddonId); + } + } + } + else if (settingId == SETTING_ALBUMSCRAPER_SETTINGS) + CGUIDialogAddonSettings::ShowForAddon(m_albumscraper, false); + else if (settingId == SETTING_ARTISTSCRAPER_SETTINGS) + CGUIDialogAddonSettings::ShowForAddon(m_artistscraper, false); + else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER) + { + VECSOURCES shares; + CServiceBroker::GetMediaManager().GetLocalDrives(shares); + CServiceBroker::GetMediaManager().GetNetworkLocations(shares); + CServiceBroker::GetMediaManager().GetRemovableDrives(shares); + std::string strDirectory = m_strArtistInfoPath; + if (!strDirectory.empty()) + { + URIUtils::AddSlashAtEnd(strDirectory); + bool bIsSource; + if (CUtil::GetMatchingSource(strDirectory, shares, bIsSource) < 0) // path is outside shares - add it as a separate one + { + CMediaSource share; + share.strName = g_localizeStrings.Get(13278); + share.strPath = strDirectory; + shares.push_back(share); + } + } + else + strDirectory = "default location"; + + if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(20223), strDirectory, true)) + { + if (!strDirectory.empty()) + { + m_strArtistInfoPath = strDirectory; + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, strDirectory); + SetFocus(CSettings::CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + } + } + } +} + +bool CGUIDialogInfoProviderSettings::Save() +{ + if (m_showSingleScraper) + return true; //Save done by caller of ::Show + + // Save default settings for fetching additional information and art + CLog::Log(LOGINFO, "{} called", __FUNCTION__); + // Save Fetch addiitional info during update + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->SetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, m_fetchInfo); + // Save default scrapers and addon setting values + settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->ID()); + m_albumscraper->SaveSettings(); + settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->ID()); + m_artistscraper->SaveSettings(); + // Save artist information folder + settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath); + settings->Save(); + + return true; +} + +void CGUIDialogInfoProviderSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); + + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath); + + if (!m_showSingleScraper) + { + SetHeading(38330); + if (!m_fetchInfo) + { + ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, false); + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false); + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, false); + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false); + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, false); + } + else + { // Album scraper + ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true); + if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID())) + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name()); + if (m_albumscraper && m_albumscraper->HasSettings()) + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true); + else + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false); + } + else + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None" + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false); + } + // Artist scraper + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true); + if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID())) + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name()); + if (m_artistscraper && m_artistscraper->HasSettings()) + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true); + else + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false); + } + else + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None" + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false); + } + // Artist Information Folder + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, true); + } + } + else if (m_singleScraperType == CONTENT_ALBUMS) + { + SetHeading(38331); + // Album scraper + ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true); + if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID())) + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name()); + if (m_albumscraper && m_albumscraper->HasSettings()) + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true); + else + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false); + } + else + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None" + ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false); + } + } + else + { + SetHeading(38332); + // Artist scraper + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true); + if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID())) + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name()); + if (m_artistscraper && m_artistscraper->HasSettings()) + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true); + else + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false); + } + else + { + SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None" + ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false); + } + // Artist Information Folder when default settings + ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_applyToItems == INFOPROVIDER_DEFAULT); + } +} + +void CGUIDialogInfoProviderSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + std::shared_ptr<CSettingCategory> category = AddCategory("infoprovidersettings", -1); + if (category == nullptr) + { + CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__); + return; + } + std::shared_ptr<CSettingGroup> group1 = AddGroup(category); + if (group1 == nullptr) + { + CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__); + return; + } + + if (!m_showSingleScraper) + { + AddToggle(group1, CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, 38333, SettingLevel::Basic, m_fetchInfo); // "Fetch additional information during scan" + } + else + { + TranslatableIntegerSettingOptions entries; + entries.clear(); + if (m_singleScraperType == CONTENT_ALBUMS) + { + entries.push_back(TranslatableIntegerSettingOption(38066, INFOPROVIDER_THISITEM)); + entries.push_back(TranslatableIntegerSettingOption(38067, INFOPROVIDER_ALLVIEW)); + } + else + { + entries.push_back(TranslatableIntegerSettingOption(38064, INFOPROVIDER_THISITEM)); + entries.push_back(TranslatableIntegerSettingOption(38065, INFOPROVIDER_ALLVIEW)); + } + entries.push_back(TranslatableIntegerSettingOption(38063, INFOPROVIDER_DEFAULT)); + AddList(group1, SETTING_APPLYTOITEMS, 38338, SettingLevel::Basic, m_applyToItems, entries, 38339); // "Apply settings to" + } + + std::shared_ptr<CSettingGroup> group = AddGroup(category, 38337); + if (group == nullptr) + { + CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__); + return; + } + std::shared_ptr<CSettingAction> subsetting; + if (!m_showSingleScraper || m_singleScraperType == CONTENT_ALBUMS) + { + AddButton(group, CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, 38334, SettingLevel::Basic); //Provider for album information + subsetting = AddButton(group, SETTING_ALBUMSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings" + if (subsetting) + subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER); + } + if (!m_showSingleScraper || m_singleScraperType == CONTENT_ARTISTS) + { + AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, 38335, SettingLevel::Basic); //Provider for artist information + subsetting = AddButton(group, SETTING_ARTISTSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings" + if (subsetting) + subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER); + + AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, 38336, SettingLevel::Basic); + } +} + +void CGUIDialogInfoProviderSettings::SetLabel2(const std::string &settingid, const std::string &label) +{ + BaseSettingControlPtr settingControl = GetSettingControl(settingid); + if (settingControl != NULL && settingControl->GetControl() != NULL) + SET_CONTROL_LABEL2(settingControl->GetID(), label); +} + +void CGUIDialogInfoProviderSettings::ToggleState(const std::string &settingid, bool enabled) +{ + BaseSettingControlPtr settingControl = GetSettingControl(settingid); + if (settingControl != NULL && settingControl->GetControl() != NULL) + { + if (enabled) + CONTROL_ENABLE(settingControl->GetID()); + else + CONTROL_DISABLE(settingControl->GetID()); + } +} + +void CGUIDialogInfoProviderSettings::SetFocus(const std::string &settingid) +{ + BaseSettingControlPtr settingControl = GetSettingControl(settingid); + if (settingControl != NULL && settingControl->GetControl() != NULL) + SET_CONTROL_FOCUS(settingControl->GetID(), 0); +} + +void CGUIDialogInfoProviderSettings::ResetDefaults() +{ + m_showSingleScraper = false; + m_singleScraperType = CONTENT_NONE; + m_applyToItems = INFOPROVIDER_THISITEM; +} diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h new file mode 100644 index 0000000..56a33b5 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/Addon.h" +#include "addons/Scraper.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <map> +#include <utility> + +class CFileItemList; + +// Enumeration of what combination of items to apply the scraper settings +enum INFOPROVIDERAPPLYOPTIONS +{ + INFOPROVIDER_DEFAULT = 0x0000, + INFOPROVIDER_ALLVIEW = 0x0001, + INFOPROVIDER_THISITEM = 0x0002 +}; + +class CGUIDialogInfoProviderSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogInfoProviderSettings(); + + // specialization of CGUIWindow + bool HasListItems() const override { return true; } + + const ADDON::ScraperPtr& GetAlbumScraper() const { return m_albumscraper; } + void SetAlbumScraper(ADDON::ScraperPtr scraper) { m_albumscraper = std::move(scraper); } + const ADDON::ScraperPtr& GetArtistScraper() const { return m_artistscraper; } + void SetArtistScraper(ADDON::ScraperPtr scraper) { m_artistscraper = std::move(scraper); } + + /*! \brief Show dialog to change information provider for either artists or albums (not both). + Has a list to select how settings are to be applied - as system default, to just current item or to all the filtered items on the node. + This does not save the settings itself, that is left to the caller + \param scraper [in/out] the selected scraper addon and settings. Scraper content must be artists or albums. + \return 0 settings apply as system default, 1 to all items on node, 2 to just the selected item or -1 if dialog cancelled or error occurs + */ + static int Show(ADDON::ScraperPtr& scraper); + + /*! \brief Show dialog to change the music scraping settings including default information providers for both artists or albums. + This saves the settings when the dialog is confirmed. + \return true if the dialog is confirmed, false otherwise + */ + static bool Show(); + +protected: + // specializations of CGUIWindow + void OnInitWindow() override; + + // implementations of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + void SetLabel2(const std::string &settingid, const std::string &label); + void ToggleState(const std::string &settingid, bool enabled); + using CGUIDialogSettingsManualBase::SetFocus; + void SetFocus(const std::string &settingid); + void ResetDefaults(); + + /*! + * @brief The currently selected album scraper + */ + ADDON::ScraperPtr m_albumscraper; + /*! + * @brief The currently selected artist scraper + */ + ADDON::ScraperPtr m_artistscraper; + + std::string m_strArtistInfoPath; + bool m_showSingleScraper = false; + CONTENT_TYPE m_singleScraperType = CONTENT_NONE; + bool m_fetchInfo; + unsigned int m_applyToItems = INFOPROVIDER_THISITEM; +}; diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.cpp b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp new file mode 100644 index 0000000..a4b282a --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp @@ -0,0 +1,1043 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogMusicInfo.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogProgress.h" +#include "filesystem/Directory.h" +#include "filesystem/MusicDatabaseDirectory/QueryParams.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDatabase.h" +#include "music/MusicLibraryQueue.h" +#include "music/MusicThumbLoader.h" +#include "music/MusicUtils.h" +#include "music/dialogs/GUIDialogSongInfo.h" +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/tags/MusicInfoTag.h" +#include "music/windows/GUIWindowMusicBase.h" +#include "profiles/ProfileManager.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileExtensionProvider.h" +#include "utils/FileUtils.h" +#include "utils/ProgressJob.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE; +using namespace MUSIC_INFO; +using namespace MUSICDATABASEDIRECTORY; +using namespace KODI::MESSAGING; + +#define CONTROL_BTN_REFRESH 6 +#define CONTROL_USERRATING 7 +#define CONTROL_BTN_PLAY 8 +#define CONTROL_BTN_GET_THUMB 10 +#define CONTROL_ARTISTINFO 12 + +#define CONTROL_LIST 50 + +#define TIME_TO_BUSY_DIALOG 500 + +class CGetInfoJob : public CJob +{ +public: + ~CGetInfoJob(void) override = default; + + // Fetch full album/artist information including art types list + bool DoWork() override + { + CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager(). + GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO); + if (!dialog) + return false; + if (dialog->IsCancelled()) + return false; + CFileItemPtr m_item = dialog->GetCurrentListItem(); + CMusicInfoTag& tag = *m_item->GetMusicInfoTag(); + + CMusicDatabase database; + database.Open(); + // May only have partially populated music item, so fetch all artist or album data from db + if (tag.GetType() == MediaTypeArtist) + { + int artistId = tag.GetDatabaseId(); + CArtist artist; + if (!database.GetArtist(artistId, artist)) + return false; + tag.SetArtist(artist); + CMusicDatabase::SetPropertiesFromArtist(*m_item, artist); + m_item->SetLabel(artist.strArtist); + + // Get artist folder where local art could be found + // Get the *name* of the folder for this artist within the Artist Info folder (may not exist). + // If there is no Artist Info folder specified in settings this will be blank + database.GetArtistPath(artist, artist.strPath); + // Get the old location for those album artists with a unique folder (local to music files) + // If there is no folder for the artist and *only* the artist this will be blank + std::string oldartistpath; + bool oldpathfound = database.GetOldArtistPath(artist.idArtist, oldartistpath); + + // Set up path for *item folder when browsing for art, by default this is + // in the Artist Info Folder (when it exists), but could end up blank + std::string artistItemPath = artist.strPath; + if (!CDirectory::Exists(artistItemPath)) + { + // Fall back local to music files (historic location for those album artists with a unique folder) + // although there may not be such a unique folder for the arist + if (oldpathfound) + artistItemPath = oldartistpath; + else + // Fall back further to browse the Artist Info Folder itself + artistItemPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + } + m_item->SetPath(artistItemPath); + + // Store info as CArtist as well as item properties + dialog->SetArtist(artist, oldartistpath); + + // Fetch artist discography as scraped from online sources, but always + // include all the albums in the music library + dialog->SetDiscography(database); + } + else + { + // tag.GetType == MediaTypeAlbum + int albumId = tag.GetDatabaseId(); + CAlbum album; + if (!database.GetAlbum(albumId, album)) + return false; + tag.SetAlbum(album); + CMusicDatabase::SetPropertiesFromAlbum(*m_item, album); + + // Get album folder where local art could be found + database.GetAlbumPath(albumId, album.strPath); + // Set up path for *item folder when browsing for art + m_item->SetPath(album.strPath); + // Store info as CAlbum as well as item properties + dialog->SetAlbum(album, album.strPath); + + // Set the list of songs and related art + dialog->SetSongs(album.songs); + } + database.Close(); + + /* + Load current art (to CGUIListItem.m_art) + For albums this includes related artist(s) art and artist fanart set as + fallback album fanart. + Clear item art first to ensure fresh not cached/partial art + */ + m_item->ClearArt(); + CMusicThumbLoader loader; + loader.LoadItem(m_item.get()); + + // Fill vector of possible art types with current art, when it exists, + // for display on the art type selection dialog + CFileItemList artlist; + MUSIC_UTILS::FillArtTypesList(*m_item, artlist); + dialog->SetArtTypeList(artlist); + if (dialog->IsCancelled()) + return false; + + // Tell waiting MusicDialog that job is complete + dialog->FetchComplete(); + + return true; + } +}; + +class CSetUserratingJob : public CJob +{ + int idAlbum; + int iUserrating; +public: + CSetUserratingJob(int albumId, int userrating) : + idAlbum(albumId), + iUserrating(userrating) + { } + + ~CSetUserratingJob(void) override = default; + + bool DoWork(void) override + { + // Asynchronously update userrating in library + CMusicDatabase db; + if (db.Open()) + { + db.SetAlbumUserrating(idAlbum, iUserrating); + db.Close(); + } + + return true; + } +}; + +class CRefreshInfoJob : public CProgressJob +{ +public: + CRefreshInfoJob(CGUIDialogProgress* progressDialog) + : CProgressJob(nullptr) + { + if (progressDialog) + SetProgressIndicators(nullptr, progressDialog); + SetAutoClose(true); + } + + ~CRefreshInfoJob(void) override = default; + + // Refresh album/artist information including art types list + bool DoWork() override + { + CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager(). + GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO); + if (!dialog) + return false; + if (dialog->IsCancelled()) + return false; + CFileItemPtr m_item = dialog->GetCurrentListItem(); + CMusicInfoTag& tag = *m_item->GetMusicInfoTag(); + CArtist& m_artist = dialog->GetArtist(); + CAlbum& m_album = dialog->GetAlbum(); + + CGUIDialogProgress* dlgProgress = GetProgressDialog(); + CMusicDatabase database; + database.Open(); + if (tag.GetType() == MediaTypeArtist) + { + ADDON::ScraperPtr scraper; + if (!database.GetScraper(m_artist.idArtist, CONTENT_ARTISTS, scraper)) + return false; + + if (dlgProgress->IsCanceled()) + return false; + database.ClearArtistLastScrapedTime(m_artist.idArtist); + + if (dlgProgress->IsCanceled()) + return false; + CMusicInfoScanner scanner; + if (scanner.UpdateArtistInfo(m_artist, scraper, true, dlgProgress) != CInfoScanner::INFO_ADDED) + return false; + else + // Tell info dialog, so can show message + dialog->SetScrapedInfo(true); + + if (dlgProgress->IsCanceled()) + return false; + //That changed DB and m_artist, now update dialog item with new info and art + tag.SetArtist(m_artist); + CMusicDatabase::SetPropertiesFromArtist(*m_item, m_artist); + + // Fetch artist discography as scraped from online sources, but always + // include all the albums in the music library + dialog->SetDiscography(database); + } + else + { + // tag.GetType == MediaTypeAlbum + ADDON::ScraperPtr scraper; + if (!database.GetScraper(m_album.idAlbum, CONTENT_ALBUMS, scraper)) + return false; + + if (dlgProgress->IsCanceled()) + return false; + database.ClearAlbumLastScrapedTime(m_album.idAlbum); + + if (dlgProgress->IsCanceled()) + return false; + CMusicInfoScanner scanner; + if (scanner.UpdateAlbumInfo(m_album, scraper, true, GetProgressDialog()) != CInfoScanner::INFO_ADDED) + return false; + else + // Tell info dialog, so can show message + dialog->SetScrapedInfo(true); + + if (dlgProgress->IsCanceled()) + return false; + //That changed DB and m_album, now update dialog item with new info and art + // Album songs are unchanged by refresh (even with Musicbrainz sync?) + tag.SetAlbum(m_album); + CMusicDatabase::SetPropertiesFromAlbum(*m_item, m_album); + + // Set the list of songs and related art + dialog->SetSongs(m_album.songs); + } + database.Close(); + + if (dlgProgress->IsCanceled()) + return false; + /* + Load current art (to CGUIListItem.m_art) + For albums this includes related artist(s) art and artist fanart set as + fallback album fanart. + Clear item art first to ensure fresh not cached/partial art + */ + m_item->ClearArt(); + CMusicThumbLoader loader; + loader.LoadItem(m_item.get()); + + if (dlgProgress->IsCanceled()) + return false; + // Fill vector of possible art types with current art, when it exists, + // for display on the art type selection dialog + CFileItemList artlist; + MUSIC_UTILS::FillArtTypesList(*m_item, artlist); + dialog->SetArtTypeList(artlist); + if (dialog->IsCancelled()) + return false; + + // Tell waiting MusicDialog that job is complete + MarkFinished(); + return true; + } +}; + +CGUIDialogMusicInfo::CGUIDialogMusicInfo(void) + : CGUIDialog(WINDOW_DIALOG_MUSIC_INFO, "DialogMusicInfo.xml"), + m_albumSongs(new CFileItemList), + m_item(new CFileItem), + m_artTypeList(new CFileItemList) +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogMusicInfo::~CGUIDialogMusicInfo(void) +{ +} + +bool CGUIDialogMusicInfo::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + { + m_artTypeList->Clear(); + // For albums update user rating if it has changed + if (!m_bArtistInfo && m_startUserrating != m_item->GetMusicInfoTag()->GetUserrating()) + { + m_hasUpdatedUserrating = true; + + // Asynchronously update song userrating in library + CSetUserratingJob *job = new CSetUserratingJob(m_item->GetMusicInfoTag()->GetAlbumId(), + m_item->GetMusicInfoTag()->GetUserrating()); + CServiceBroker::GetJobManager()->AddJob(job, nullptr); + } + if (m_hasRefreshed || m_hasUpdatedUserrating) + { + // Send a message to all windows to tell them to update the item. + // This communicates changes to the music lib window. + // The music lib window item is updated to but changes to the rating when it is the sort + // do not show on screen until refresh() that fetches the list from scratch, sorts etc. + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } + + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST); + OnMessage(msg); + m_albumSongs->Clear(); + } + break; + + case GUI_MSG_WINDOW_INIT: + { + CGUIDialog::OnMessage(message); + Update(); + m_cancelled = false; + return true; + } + break; + + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_USERRATING) + { + OnSetUserrating(); + } + else if (iControl == CONTROL_BTN_REFRESH) + { + RefreshInfo(); + return true; + } + else if (iControl == CONTROL_BTN_GET_THUMB) + { + OnGetArt(); + return true; + } + else if (iControl == CONTROL_ARTISTINFO) + { + if (!m_bArtistInfo) + OnArtistInfo(m_album.artistCredits[0].GetArtistId()); + return true; + } + else if (iControl == CONTROL_LIST) + { + int iAction = message.GetParam1(); + if (m_bArtistInfo && (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction)) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + int iItem = msg.GetParam1(); + int id = -1; + if (iItem >= 0 && iItem < m_albumSongs->Size()) + id = m_albumSongs->Get(iItem)->GetMusicInfoTag()->GetDatabaseId(); + if (id > 0) + { + OnAlbumInfo(id); + return true; + } + } + } + else if (iControl == CONTROL_BTN_PLAY) + { + if (m_album.idAlbum >= 0) + { + // Play album + const std::string path = StringUtils::Format("musicdb://albums/{}", m_album.idAlbum); + OnPlayItem(std::make_shared<CFileItem>(path, m_album)); + return true; + } + else + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + const int iItem = msg.GetParam1(); + if (iItem >= 0 && iItem < m_albumSongs->Size()) + { + // Play selected song + OnPlayItem(m_albumSongs->Get(iItem)); + return true; + } + } + return false; + } + } + break; + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogMusicInfo::OnAction(const CAction &action) +{ + int userrating = m_item->GetMusicInfoTag()->GetUserrating(); + if (action.GetID() == ACTION_INCREASE_RATING) + { + SetUserrating(userrating + 1); + return true; + } + else if (action.GetID() == ACTION_DECREASE_RATING) + { + SetUserrating(userrating - 1); + return true; + } + else if (action.GetID() == ACTION_SHOW_INFO) + { + Close(); + return true; + } + return CGUIDialog::OnAction(action); +} + +bool CGUIDialogMusicInfo::SetItem(CFileItem* item) +{ + *m_item = *item; + m_event.Reset(); + m_cancelled = false; // Happens before win_init + + // In a separate job fetch info and fill list of art types. + int jobid = + CServiceBroker::GetJobManager()->AddJob(new CGetInfoJob(), nullptr, CJob::PRIORITY_LOW); + + // Wait to get all data before show, allowing user to cancel if fetch is slow + if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG)) + { + // Cancel job still waiting in queue (unlikely) + CServiceBroker::GetJobManager()->CancelJob(jobid); + // Flag to stop job already in progress + m_cancelled = true; + return false; + } + + return true; +} + +void CGUIDialogMusicInfo::SetAlbum(const CAlbum& album, const std::string &path) +{ + m_album = album; + m_item->SetPath(album.strPath); + + m_startUserrating = m_album.iUserrating; + m_fallbackartpath.clear(); + m_bArtistInfo = false; + m_hasUpdatedUserrating = false; + m_hasRefreshed = false; +} + +void CGUIDialogMusicInfo::SetArtist(const CArtist& artist, const std::string &path) +{ + m_artist = artist; + m_fallbackartpath = path; + m_bArtistInfo = true; + m_hasRefreshed = false; +} + +void CGUIDialogMusicInfo::SetSongs(const VECSONGS &songs) const +{ + m_albumSongs->Clear(); + CMusicThumbLoader loader; + for (unsigned int i = 0; i < songs.size(); i++) + { + const CSong& song = songs[i]; + CFileItemPtr item(new CFileItem(song)); + // Load the song art and related artist(s) (that may be different from album artist) art + loader.LoadItem(item.get()); + m_albumSongs->Add(item); + } +} + +void CGUIDialogMusicInfo::SetDiscography(CMusicDatabase& database) const +{ + m_albumSongs->Clear(); + database.GetArtistDiscography(m_artist.idArtist, *m_albumSongs); + CMusicThumbLoader loader; + for (const auto& item : *m_albumSongs) + { + // Load all the album art and related artist(s) art (could be other collaborating artists) + loader.LoadItem(item.get()); + if (item->GetMusicInfoTag()->GetDatabaseId() == -1) + item->SetArt("thumb", "DefaultAlbumCover.png"); + } +} + +void CGUIDialogMusicInfo::Update() +{ + if (m_bArtistInfo) + { + SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO); + SET_CONTROL_HIDDEN(CONTROL_USERRATING); + + CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get()); + OnMessage(message); + + } + else + { + SET_CONTROL_VISIBLE(CONTROL_ARTISTINFO); + SET_CONTROL_VISIBLE(CONTROL_USERRATING); + + CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get()); + OnMessage(message); + + } + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // Disable the Choose Art button if the user isn't allowed it + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB, + profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser); +} + +void CGUIDialogMusicInfo::SetLabel(int iControl, const std::string& strLabel) +{ + if (strLabel.empty()) + { + SET_CONTROL_LABEL(iControl, 416); + } + else + { + SET_CONTROL_LABEL(iControl, strLabel); + } +} + +void CGUIDialogMusicInfo::OnInitWindow() +{ + SET_CONTROL_LABEL(CONTROL_BTN_REFRESH, 184); + SET_CONTROL_LABEL(CONTROL_USERRATING, 38023); + SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511); + SET_CONTROL_LABEL(CONTROL_ARTISTINFO, 21891); + SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208); + + if (m_bArtistInfo) + { + SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO); + SET_CONTROL_HIDDEN(CONTROL_USERRATING); + SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY); + } + CGUIDialog::OnInitWindow(); +} + +void CGUIDialogMusicInfo::FetchComplete() +{ + //Trigger the event to indicate data has been fetched + m_event.Set(); +} + + +void CGUIDialogMusicInfo::RefreshInfo() +{ + // Double check we have permission (button should be hidden when not) + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + if (!profileManager->GetCurrentProfile().canWriteDatabases() && !g_passwordManager.bMasterUser) + return; + + // Check if scanning + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 }); + return; + } + + CGUIDialogProgress* dlgProgress = CServiceBroker::GetGUI()->GetWindowManager(). + GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (!dlgProgress) + return; + + if (m_bArtistInfo) + { // Show dialog box indicating we're searching for the artist + dlgProgress->SetHeading(CVariant{ 21889 }); + dlgProgress->SetLine(0, CVariant{ m_artist.strArtist }); + dlgProgress->SetLine(1, CVariant{ "" }); + dlgProgress->SetLine(2, CVariant{ "" }); + } + else + { // Show dialog box indicating we're searching for the album + dlgProgress->SetHeading(CVariant{ 185 }); + dlgProgress->SetLine(0, CVariant{ m_album.strAlbum }); + dlgProgress->SetLine(1, CVariant{ m_album.strArtistDesc }); + dlgProgress->SetLine(2, CVariant{ "" }); + } + dlgProgress->Open(); + + SetScrapedInfo(false); + // Start separate job to scrape info and fill list of art types. + CServiceBroker::GetJobManager()->AddJob(new CRefreshInfoJob(dlgProgress), nullptr, + CJob::PRIORITY_HIGH); + + // Wait for refresh to complete or be canceled, but render every 10ms so that the + // pointer movements works on dialog even when job is reporting progress infrequently + if (dlgProgress) + dlgProgress->Wait(10); + + if (dlgProgress->IsCanceled()) + { + return; + } + + // Show message when scraper was unsuccessful + if (!HasScrapedInfo()) + { + if (m_bArtistInfo) + HELPERS::ShowOKDialogText(CVariant{ 21889 }, CVariant{ 20199 }); + else + HELPERS::ShowOKDialogText(CVariant{ 185 }, CVariant{ 500 }); + return; + } + + // Show new values on screen + Update(); + m_hasRefreshed = true; + + if (dlgProgress) + dlgProgress->Close(); +} + +void CGUIDialogMusicInfo::SetUserrating(int userrating) const +{ + userrating = std::max(userrating, 0); + userrating = std::min(userrating, 10); + if (userrating != m_item->GetMusicInfoTag()->GetUserrating()) + { + m_item->GetMusicInfoTag()->SetUserrating(userrating); + } +} + +void CGUIDialogMusicInfo::OnAlbumInfo(int id) +{ + // Switch to show album info for given album ID + // Close current (artist) dialog to save art changes + Close(true); + ShowForAlbum(id); +} + +void CGUIDialogMusicInfo::OnArtistInfo(int id) +{ + // Switch to show artist info for given artist ID + // Close current (album) dialog to save art and rating changes + Close(true); + ShowForArtist(id); +} + +CFileItemPtr CGUIDialogMusicInfo::GetCurrentListItem(int offset) +{ + return m_item; +} + +std::string CGUIDialogMusicInfo::GetContent() +{ + if (m_item->GetMusicInfoTag()->GetType() == MediaTypeArtist) + return "artists"; + else + return "albums"; +} + +void CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item) +{ + std::string itemDir; + std::string artistFolder; + + itemDir = item.GetPath(); + if (item.HasMusicInfoTag()) + { + if (item.GetMusicInfoTag()->GetType() == MediaTypeSong) + itemDir = URIUtils::GetParentPath(item.GetMusicInfoTag()->GetURL()); + + // For artist add Artist Info Folder path to browser sources + if (item.GetMusicInfoTag()->GetType() == MediaTypeArtist) + { + artistFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER); + if (!artistFolder.empty() && artistFolder.compare(itemDir) == 0) + itemDir.clear(); // skip *item when artist not have a unique path + } + } + // Add "*Item folder" path to file browser sources + if (!itemDir.empty() && CDirectory::Exists(itemDir)) + { + CMediaSource itemSource; + itemSource.strName = g_localizeStrings.Get(36041); + itemSource.strPath = itemDir; + sources.push_back(itemSource); + } + + // For artist add Artist Info Folder path to browser sources + if (!artistFolder.empty() && CDirectory::Exists(artistFolder)) + { + CMediaSource itemSource; + itemSource.strName = "* " + g_localizeStrings.Get(20223); + itemSource.strPath = artistFolder; + sources.push_back(itemSource); + } +} + +void CGUIDialogMusicInfo::SetArtTypeList(CFileItemList& artlist) +{ + m_artTypeList->Clear(); + m_artTypeList->Copy(artlist); +} + +/* +Allow user to choose artwork for the artist or album +For each type of art the options are: +1. Current art +2. Local art (thumb found by filename) +3. Remote art (scraped list of urls from online sources e.g. fanart.tv) +5. Embedded art (@todo) +6. None +*/ +void CGUIDialogMusicInfo::OnGetArt() +{ + std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(*m_artTypeList); + if (type.empty()) + return; // Cancelled + + CFileItemList items; + CGUIListItem::ArtMap primeArt = m_item->GetArt(); // art without fallbacks + bool bHasArt = m_item->HasArt(type); + bool bFallback(false); + if (bHasArt) + { + // Check if that type of art is actually a fallback, e.g. artist fanart + CGUIListItem::ArtMap::const_iterator i = primeArt.find(type); + bFallback = (i == primeArt.end()); + } + + // Build list of possible images of that art type + if (bHasArt) + { + // Add item for current artwork + // For album it could be a fallback from artist + CFileItemPtr item(new CFileItem("thumb://Current", false)); + item->SetArt("thumb", m_item->GetArt(type)); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13512)); + items.Add(item); + } + + // Grab the thumbnails of this art type scraped from the web + std::vector<std::string> remotethumbs; + // Art type is encoded into the scraper XML as optional "aspect=" field + // Type "thumb" returns URLs for all types of art including those without aspect. + // Those URL without aspect are also returned for all other type values. + if (m_bArtistInfo) + m_artist.thumbURL.GetThumbUrls(remotethumbs, type); + else + m_album.thumbURL.GetThumbUrls(remotethumbs, type); + + for (unsigned int i = 0; i < remotethumbs.size(); ++i) + { + std::string strItemPath; + strItemPath = StringUtils::Format("thumb://Remote{}", i); + CFileItemPtr item(new CFileItem(strItemPath, false)); + item->SetArt("thumb", remotethumbs[i]); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13513)); + + items.Add(item); + } + + // Local art + std::string localArt; + std::vector<std::string> paths; + if (m_bArtistInfo) + { + // Individual artist subfolder within the Artist Information Folder + paths.emplace_back(m_artist.strPath); + // Fallback local to music files (when there is a unique folder) + paths.emplace_back(m_fallbackartpath); + } + else + // Album folder, when a unique one exists, no fallback + paths.emplace_back(m_album.strPath); + for (const auto& path : paths) + { + if (!localArt.empty() && CFileUtils::Exists(localArt)) + break; + if (!path.empty()) + { + CFileItem item(path, true); + if (type == "thumb") + // Local music thumbnail images named by <musicthumbs> + localArt = item.GetUserMusicThumb(true); + else + { // Check case and ext insenitively for local images with type as name + // e.g. <arttype>.jpg + CFileItemList items; + CDirectory::GetDirectory(path, items, + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO); + + for (int j = 0; j < items.Size(); j++) + { + std::string strCandidate = URIUtils::GetFileName(items[j]->GetPath()); + URIUtils::RemoveExtension(strCandidate); + if (StringUtils::EqualsNoCase(strCandidate, type)) + { + localArt = items[j]->GetPath(); + break; + } + } + } + } + } + if (!localArt.empty() && CFileUtils::Exists(localArt)) + { + CFileItemPtr item(new CFileItem("Local Art: " + localArt, false)); + item->SetArt("thumb", localArt); + item->SetLabel(g_localizeStrings.Get(13514)); // "Local art" + items.Add(item); + } + + // No art + if (bHasArt && !bFallback) + { // Actually has this type of art (not a fallback) so + // allow the user to delete it by selecting "no art". + CFileItemPtr item(new CFileItem("thumb://None", false)); + if (m_bArtistInfo) + item->SetArt("icon", "DefaultArtist.png"); + else + item->SetArt("icon", "DefaultAlbumCover.png"); + item->SetLabel(g_localizeStrings.Get(13515)); + items.Add(item); + } + + //! @todo: Add support for extracting embedded art from song files to use for album + + // Clear local images of this type from cache so user will see any recent + // local file changes immediately + for (auto& item : items) + { + // Skip images from remote sources, recache done by refresh (could be slow) + if (StringUtils::StartsWith(item->GetPath(), "thumb://Remote")) + continue; + std::string thumb(item->GetArt("thumb")); + if (thumb.empty()) + continue; + CURL url(CTextureUtils::UnwrapImageURL(thumb)); + // Skip images from remote sources (current thumb could be remote) + if (url.IsProtocol("http") || url.IsProtocol("https")) + continue; + CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + // Remove any thumbnail of local image too (created when browsing files) + std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb)); + CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb); + } + + // Show list of possible art for user selection + // Note that during selection thumbs of *all* images shown are cached. When + // browsing folders there could be many irrelevant thumbs cached that are + // never used by Kodi again, but there is no obvious way to clear these + // thumbs from the cache automatically. + std::string result; + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music")); + CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_item); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) && + result != "thumb://Current") + { + // User didn't choose the one they have. + // Overwrite with the new art or clear it + std::string newArt; + if (StringUtils::StartsWith(result, "thumb://Remote")) + { + int number = atoi(result.substr(14).c_str()); + newArt = remotethumbs[number]; + } + else if (result == "thumb://Thumb") + newArt = m_item->GetArt("thumb"); + else if (StringUtils::StartsWith(result, "Local Art: ")) + newArt = localArt; + else if (CFileUtils::Exists(result)) + newArt = result; + else // none + newArt.clear(); + + // Asynchronously update that type of art in the database and then + // refresh artist, album and fallback art of currently playing song + MUSIC_UTILS::UpdateArtJob(m_item, type, newArt); + + // Update local item and art list with current art + m_item->SetArt(type, newArt); + for (const auto& artitem : *m_artTypeList) + { + if (artitem->GetProperty("artType") == type) + { + artitem->SetArt("thumb", newArt); + break; + } + } + + // Get new artwork to show in other places e.g. on music lib window, but this does + // not update artist, album or fallback art for the currently playing song as it + // is a different item with different ID and media type + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } + + // Re-open the art type selection dialog as we come back from + // the image selection dialog + OnGetArt(); +} + +void CGUIDialogMusicInfo::OnSetUserrating() const +{ + int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_item->GetMusicInfoTag()->GetUserrating()); + if (userrating < 0) // Nothing selected, so rating unchanged + return; + + SetUserrating(userrating); +} + + +void CGUIDialogMusicInfo::ShowForAlbum(int idAlbum) +{ + std::string path = StringUtils::Format("musicdb://albums/{}", idAlbum); + CFileItem item(path, true); // An album, but IsAlbum() not set as didn't use SetAlbum() + ShowFor(&item); +} + +void CGUIDialogMusicInfo::ShowForArtist(int idArtist) +{ + std::string path = StringUtils::Format("musicdb://artists/{}", idArtist); + CFileItem item(path, true); + ShowFor(&item); +} + +void CGUIDialogMusicInfo::ShowFor(CFileItem* pItem) +{ + if (pItem->IsParentFolder() || URIUtils::IsSpecial(pItem->GetPath()) || + StringUtils::StartsWithNoCase(pItem->GetPath(), "musicsearch://")) + return; // nothing to do + + if (!pItem->m_bIsFolder) + { // Show Song information dialog + CGUIDialogSongInfo::ShowFor(pItem); + return; + } + + CFileItem musicitem("musicdb://", true); + + // We have a folder album/artist info dialog only shown for db items + // or for music video with artist/album in music library + if (pItem->IsMusicDb()) + { + if (!pItem->HasMusicInfoTag() || pItem->GetMusicInfoTag()->GetDatabaseId() < 1) + { + // Maybe only path is set, then set MusicInfoTag + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(pItem->GetPath(), params); + if (params.GetArtistId() > 0) + pItem->GetMusicInfoTag()->SetDatabaseId(params.GetArtistId(), MediaTypeArtist); + else if (params.GetAlbumId() > 0) + pItem->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum); + else + return; // nothing to do + } + musicitem.SetFromMusicInfoTag(*pItem->GetMusicInfoTag()); + } + else if (pItem->HasProperty("artist_musicid")) + { + musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("artist_musicid").asInteger32(), + MediaTypeArtist); + } + else if (pItem->HasProperty("album_musicid")) + { + musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("album_musicid").asInteger32(), + MediaTypeAlbum); + } + else + return; // nothing to do + + + CGUIDialogMusicInfo *pDlgMusicInfo = CServiceBroker::GetGUI()->GetWindowManager(). + GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO); + if (pDlgMusicInfo) + { + if (pDlgMusicInfo->SetItem(&musicitem)) + { + pDlgMusicInfo->Open(); + if (pItem->GetMusicInfoTag()->GetType() == MediaTypeAlbum && + pDlgMusicInfo->HasUpdatedUserrating()) + { + auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV); + if (window) + window->RefreshContent("albums"); + } + } + } +} + +void CGUIDialogMusicInfo::OnPlayItem(const std::shared_ptr<CFileItem>& item) +{ + Close(true); + MUSIC_UTILS::PlayItem(item); +} diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.h b/xbmc/music/dialogs/GUIDialogMusicInfo.h new file mode 100644 index 0000000..781a7d1 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogMusicInfo.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "MediaSource.h" +#include "guilib/GUIDialog.h" +#include "music/Album.h" +#include "music/Artist.h" +#include "music/Song.h" +#include "threads/Event.h" + +#include <memory> + +class CFileItem; +class CFileItemList; + +class CGUIDialogMusicInfo : + public CGUIDialog +{ +public: + CGUIDialogMusicInfo(void); + ~CGUIDialogMusicInfo(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + bool SetItem(CFileItem* item); + void SetAlbum(const CAlbum& album, const std::string &path); + void SetArtist(const CArtist& artist, const std::string &path); + bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; } + bool HasRefreshed() const { return m_hasRefreshed; } + + bool HasListItems() const override { return true; } + CFileItemPtr GetCurrentListItem(int offset = 0) override; + std::string GetContent(); + static void AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item); + void SetDiscography(CMusicDatabase& database) const; + void SetSongs(const VECSONGS &songs) const; + void SetArtTypeList(CFileItemList& artlist); + void SetScrapedInfo(bool bScraped) { m_scraperAddInfo = bScraped; } + CArtist& GetArtist() { return m_artist; } + CAlbum& GetAlbum() { return m_album; } + bool IsArtistInfo() const { return m_bArtistInfo; } + bool IsCancelled() const { return m_cancelled; } + bool HasScrapedInfo() const { return m_scraperAddInfo; } + void FetchComplete(); + void RefreshInfo(); + + static void ShowForAlbum(int idAlbum); + static void ShowForArtist(int idArtist); + static void ShowFor(CFileItem* pItem); +protected: + void OnInitWindow() override; + void Update(); + void SetLabel(int iControl, const std::string& strLabel); + void OnGetArt(); + void OnAlbumInfo(int id); + void OnArtistInfo(int id); + void OnSetUserrating() const; + void SetUserrating(int userrating) const; + void OnPlayItem(const std::shared_ptr<CFileItem>& item); + + CAlbum m_album; + CArtist m_artist; + int m_startUserrating = -1; + bool m_hasUpdatedUserrating = false; + bool m_hasRefreshed = false; + bool m_bArtistInfo = false; + bool m_cancelled = false; + bool m_scraperAddInfo = false; + std::unique_ptr<CFileItemList> m_albumSongs; + std::shared_ptr<CFileItem> m_item; + std::unique_ptr<CFileItemList> m_artTypeList; + CEvent m_event; + std::string m_fallbackartpath; +}; diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.cpp b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp new file mode 100644 index 0000000..6cd110b --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogMusicOSD.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "addons/addoninfo/AddonType.h" +#include "addons/gui/GUIWindowAddonBrowser.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/Key.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +#define CONTROL_VIS_BUTTON 500 +#define CONTROL_LOCK_BUTTON 501 + +CGUIDialogMusicOSD::CGUIDialogMusicOSD(void) + : CGUIDialog(WINDOW_DIALOG_MUSIC_OSD, "MusicOSD.xml") +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogMusicOSD::~CGUIDialogMusicOSD(void) = default; + +bool CGUIDialogMusicOSD::OnMessage(CGUIMessage &message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + unsigned int iControl = message.GetSenderId(); + if (iControl == CONTROL_VIS_BUTTON) + { + std::string addonID; + if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::VISUALIZATION, addonID, true) == + 1) + { + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->SetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION, addonID); + settings->Save(); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_VISUALISATION_RELOAD, 0, 0); + } + } + else if (iControl == CONTROL_LOCK_BUTTON) + { + CGUIMessage msg(GUI_MSG_VISUALISATION_ACTION, 0, 0, ACTION_VIS_PRESET_LOCK); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } + return true; + } + break; + } + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogMusicOSD::OnAction(const CAction &action) +{ + switch (action.GetID()) + { + case ACTION_SHOW_OSD: + Close(); + return true; + default: + break; + } + + return CGUIDialog::OnAction(action); +} + +void CGUIDialogMusicOSD::FrameMove() +{ + if (m_autoClosing) + { + // check for movement of mouse or a submenu open + if (CServiceBroker::GetInputManager().IsMouseActive() || + CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_SETTINGS) || + CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_PRESET_LIST) || + CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_RADIO_RDS_INFO)) + // extend show time by original value + SetAutoClose(m_showDuration); + } + CGUIDialog::FrameMove(); +} diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.h b/xbmc/music/dialogs/GUIDialogMusicOSD.h new file mode 100644 index 0000000..99a208c --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogMusicOSD.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/GUIDialog.h" + +class CGUIDialogMusicOSD : + public CGUIDialog +{ +public: + CGUIDialogMusicOSD(void); + ~CGUIDialogMusicOSD(void) override; + bool OnMessage(CGUIMessage &message) override; + bool OnAction(const CAction &action) override; + void FrameMove() override; +}; diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.cpp b/xbmc/music/dialogs/GUIDialogSongInfo.cpp new file mode 100644 index 0000000..8efca94 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogSongInfo.cpp @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogSongInfo.h" + +#include "GUIDialogMusicInfo.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "Util.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "music/MusicDatabase.h" +#include "music/MusicUtils.h" +#include "music/tags/MusicInfoTag.h" +#include "music/windows/GUIWindowMusicBase.h" +#include "profiles/ProfileManager.h" +#include "settings/MediaSourceSettings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" + +#define CONTROL_BTN_REFRESH 6 +#define CONTROL_USERRATING 7 +#define CONTROL_BTN_PLAY 8 +#define CONTROL_BTN_GET_THUMB 10 +#define CONTROL_ALBUMINFO 12 + +#define CONTROL_LIST 50 + +#define TIME_TO_BUSY_DIALOG 500 + + + +class CGetSongInfoJob : public CJob +{ +public: + ~CGetSongInfoJob(void) override = default; + + // Fetch full song information including art types list + bool DoWork() override + { + CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO); + if (!dialog) + return false; + if (dialog->IsCancelled()) + return false; + CFileItemPtr m_song = dialog->GetCurrentListItem(); + + // Fetch tag data from library using filename of item path, or scanning file + // (if item does not already have this loaded) + if (!m_song->LoadMusicTag()) + { + // Stop SongInfoDialog waiting + dialog->FetchComplete(); + return false; + } + if (dialog->IsCancelled()) + return false; + // Fetch album and primary song artist data from library as properties + // and lyrics by scanning tags from file + MUSIC_INFO::CMusicInfoLoader::LoadAdditionalTagInfo(m_song.get()); + if (dialog->IsCancelled()) + return false; + + // Get album path (for use in browsing art selection) + std::string albumpath; + CMusicDatabase db; + db.Open(); + db.GetAlbumPath(m_song->GetMusicInfoTag()->GetAlbumId(), albumpath); + m_song->SetProperty("album_path", albumpath); + db.Close(); + if (dialog->IsCancelled()) + return false; + + // Load song art. + // For songs in library this includes related album and artist(s) art. + // Also fetches artist art for non library songs when artist can be found + // uniquely by name, otherwise just embedded or cached thumb is fetched. + CMusicThumbLoader loader; + loader.LoadItem(m_song.get()); + if (dialog->IsCancelled()) + return false; + + // For songs in library fill vector of possible art types, with current art when it exists + // for display on the art type selection dialog + CFileItemList artlist; + MUSIC_UTILS::FillArtTypesList(*m_song, artlist); + dialog->SetArtTypeList(artlist); + if (dialog->IsCancelled()) + return false; + + // Tell waiting SongInfoDialog that job is complete + dialog->FetchComplete(); + + return true; + } +}; + +CGUIDialogSongInfo::CGUIDialogSongInfo(void) + : CGUIDialog(WINDOW_DIALOG_SONG_INFO, "DialogMusicInfo.xml") + , m_song(new CFileItem) +{ + m_cancelled = false; + m_hasUpdatedUserrating = false; + m_startUserrating = -1; + m_artTypeList.Clear(); + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogSongInfo::~CGUIDialogSongInfo(void) = default; + +bool CGUIDialogSongInfo::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_DEINIT: + { + m_artTypeList.Clear(); + if (m_startUserrating != m_song->GetMusicInfoTag()->GetUserrating()) + { + m_hasUpdatedUserrating = true; + + // Asynchronously update song userrating in library + MUSIC_UTILS::UpdateSongRatingJob(m_song, m_song->GetMusicInfoTag()->GetUserrating()); + + // Send a message to all windows to tell them to update the fileitem + // This communicates the rating change to the music lib window, current playlist and OSD. + // The music lib window item is updated to but changes to the rating when it is the sort + // do not show on screen until refresh() that fetches the list from scratch, sorts etc. + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST); + OnMessage(msg); + break; + } + case GUI_MSG_WINDOW_INIT: + CGUIDialog::OnMessage(message); + Update(); + m_cancelled = false; + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_USERRATING) + { + OnSetUserrating(); + } + else if (iControl == CONTROL_ALBUMINFO) + { + CGUIDialogMusicInfo::ShowForAlbum(m_albumId); + return true; + } + else if (iControl == CONTROL_BTN_GET_THUMB) + { + OnGetArt(); + return true; + } + else if (iControl == CONTROL_LIST) + { + int iAction = message.GetParam1(); + if ((ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction)) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + int iItem = msg.GetParam1(); + if (iItem < 0 || iItem >= static_cast<int>(m_song->GetMusicInfoTag()->GetContributors().size())) + break; + int idArtist = m_song->GetMusicInfoTag()->GetContributors()[iItem].GetArtistId(); + if (idArtist > 0) + CGUIDialogMusicInfo::ShowForArtist(idArtist); + return true; + } + } + else if (iControl == CONTROL_BTN_PLAY) + { + OnPlaySong(m_song); + return true; + } + return false; + } + break; + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogSongInfo::OnAction(const CAction& action) +{ + int userrating = m_song->GetMusicInfoTag()->GetUserrating(); + if (action.GetID() == ACTION_INCREASE_RATING) + { + SetUserrating(userrating + 1); + return true; + } + else if (action.GetID() == ACTION_DECREASE_RATING) + { + SetUserrating(userrating - 1); + return true; + } + else if (action.GetID() == ACTION_SHOW_INFO) + { + Close(); + return true; + } + return CGUIDialog::OnAction(action); +} + +bool CGUIDialogSongInfo::OnBack(int actionID) +{ + m_cancelled = true; + return CGUIDialog::OnBack(actionID); +} + +void CGUIDialogSongInfo::FetchComplete() +{ + //Trigger the event to indicate data has been fetched + m_event.Set(); +} + +void CGUIDialogSongInfo::OnInitWindow() +{ + // Enable album info button when we know album + m_albumId = m_song->GetMusicInfoTag()->GetAlbumId(); + + CONTROL_ENABLE_ON_CONDITION(CONTROL_ALBUMINFO, m_albumId > 0); + + // Disable music user rating button for plugins as they don't have tables to save this + if (m_song->IsPlugin()) + CONTROL_DISABLE(CONTROL_USERRATING); + else + CONTROL_ENABLE(CONTROL_USERRATING); + + // Disable the Choose Art button if the user isn't allowed it + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB, + profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser); + + SET_CONTROL_HIDDEN(CONTROL_BTN_REFRESH); + SET_CONTROL_LABEL(CONTROL_USERRATING, 38023); + SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511); + SET_CONTROL_LABEL(CONTROL_ALBUMINFO, 10523); + SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208); + + CGUIDialog::OnInitWindow(); +} + +void CGUIDialogSongInfo::Update() +{ + CFileItemList items; + for (const auto& contributor : m_song->GetMusicInfoTag()->GetContributors()) + { + auto item = std::make_shared<CFileItem>(contributor.GetRoleDesc()); + item->SetLabel2(contributor.GetArtist()); + item->GetMusicInfoTag()->SetDatabaseId(contributor.GetArtistId(), MediaTypeArtist); + items.Add(std::move(item)); + } + CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, &items); + OnMessage(message); +} + +void CGUIDialogSongInfo::SetUserrating(int userrating) +{ + userrating = std::max(userrating, 0); + userrating = std::min(userrating, 10); + if (userrating != m_song->GetMusicInfoTag()->GetUserrating()) + { + m_song->GetMusicInfoTag()->SetUserrating(userrating); + } +} + +bool CGUIDialogSongInfo::SetSong(CFileItem* item) +{ + *m_song = *item; + m_event.Reset(); + m_cancelled = false; // SetSong happens before win_init + // In a separate job fetch song info and fill list of art types. + int jobid = + CServiceBroker::GetJobManager()->AddJob(new CGetSongInfoJob(), nullptr, CJob::PRIORITY_LOW); + + // Wait to get all data before show, allowing user to cancel if fetch is slow + if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG)) + { + // Cancel job still waiting in queue (unlikely) + CServiceBroker::GetJobManager()->CancelJob(jobid); + // Flag to stop job already in progress + m_cancelled = true; + return false; + } + + // Store initial userrating + m_startUserrating = m_song->GetMusicInfoTag()->GetUserrating(); + m_hasUpdatedUserrating = false; + return true; +} + +void CGUIDialogSongInfo::SetArtTypeList(CFileItemList& artlist) +{ + m_artTypeList.Copy(artlist); +} + +CFileItemPtr CGUIDialogSongInfo::GetCurrentListItem(int offset) +{ + return m_song; +} + +std::string CGUIDialogSongInfo::GetContent() +{ + return "songs"; +} + +/* + Allow user to choose artwork for the song + For each type of art the options are: + 1. Current art + 2. Local art (thumb found by filename) + 3. Embedded art (@todo) + 4. None + Note that songs are not scraped, hence there is no list of urls for possible remote art +*/ +void CGUIDialogSongInfo::OnGetArt() +{ + std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(m_artTypeList); + if (type.empty()) + return; // Cancelled + + CFileItemList items; + CGUIListItem::ArtMap primeArt = m_song->GetArt(); // Song art without fallbacks + bool bHasArt = m_song->HasArt(type); + bool bFallback(false); + if (bHasArt) + { + // Check if that type of art is actually a fallback, e.g. album thumb or artist fanart + CGUIListItem::ArtMap::const_iterator i = primeArt.find(type); + bFallback = (i == primeArt.end()); + } + + // Build list of possible images of that art type + if (bHasArt) + { + // Add item for current artwork, could a fallback from album/artist + CFileItemPtr item(new CFileItem("thumb://Current", false)); + item->SetArt("thumb", m_song->GetArt(type)); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13512)); //! @todo: label fallback art so user knows? + items.Add(item); + } + else if (m_song->HasArt("thumb")) + { // For missing art of that type add the thumb (when it exists and not a fallback) + CGUIListItem::ArtMap::const_iterator i = primeArt.find("thumb"); + if (i != primeArt.end()) + { + CFileItemPtr item(new CFileItem("thumb://Thumb", false)); + item->SetArt("thumb", m_song->GetArt("thumb")); + item->SetArt("icon", "DefaultAlbumCover.png"); + item->SetLabel(g_localizeStrings.Get(21371)); + items.Add(item); + } + } + + std::string localThumb; + if (type == "thumb") + { // Local thumb type art held in <filename>.tbn (for non-library items) + localThumb = m_song->GetUserMusicThumb(true); + if (m_song->IsMusicDb()) + { + CFileItem item(m_song->GetMusicInfoTag()->GetURL(), false); + localThumb = item.GetUserMusicThumb(true); + } + if (CFileUtils::Exists(localThumb)) + { + CFileItemPtr item(new CFileItem("thumb://Local", false)); + item->SetArt("thumb", localThumb); + item->SetLabel(g_localizeStrings.Get(20017)); + items.Add(item); + } + } + + // Clear these local images from cache so user will see any recent + // local file changes immediately + for (auto& item : items) + { + std::string thumb(item->GetArt("thumb")); + if (thumb.empty()) + continue; + CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + // Remove any thumbnail of local image too (created when browsing files) + std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb)); + CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb); + } + + if (bHasArt && !bFallback) + { // Actually has this type of art (not a fallback) so + // allow the user to delete it by selecting "no art". + CFileItemPtr item(new CFileItem("thumb://None", false)); + item->SetArt("thumb", "DefaultAlbumCover.png"); + item->SetLabel(g_localizeStrings.Get(13515)); + items.Add(item); + } + + //! @todo: Add support for extracting embedded art + + // Show list of possible art for user selection + std::string result; + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music")); + // Add album folder as source (could be disc set) + std::string albumpath = m_song->GetProperty("album_path").asString(); + if (!albumpath.empty()) + { + CFileItem pathItem(albumpath, true); + CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, pathItem); + } + else // Add parent folder of song + CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_song); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) && + result != "thumb://Current") + { + // User didn't choose the one they have, or the fallback image. + // Overwrite with the new art or clear it + std::string newArt; + if (result == "thumb://Thumb") + newArt = m_song->GetArt("thumb"); + else if (result == "thumb://Local") + newArt = localThumb; +// else if (result == "thumb://Embedded") +// newArt = embeddedArt; + else if (CFileUtils::Exists(result)) + newArt = result; + else // none + newArt.clear(); + + // Asynchronously update that type of art in the database + MUSIC_UTILS::UpdateArtJob(m_song, type, newArt); + + // Update local song with current art + if (newArt.empty()) + { + // Remove that type of art from the song + primeArt.erase(type); + m_song->SetArt(primeArt); + } + else + // Add or modify the type of art + m_song->SetArt(type, newArt); + + // Update local art list with current art + // Show any fallback art when song art removed + if (newArt.empty() && m_song->HasArt(type)) + newArt = m_song->GetArt(type); + for (const auto& artitem : m_artTypeList) + { + if (artitem->GetProperty("artType") == type) + { + artitem->SetArt("thumb", newArt); + break; + } + } + + // Get new artwork to show in other places e.g. on music lib window, + // current playlist and player OSD. + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + + } + + // Re-open the art type selection dialog as we come back from + // the image selection dialog + OnGetArt(); +} + +void CGUIDialogSongInfo::OnSetUserrating() +{ + int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_song->GetMusicInfoTag()->GetUserrating()); + if (userrating < 0) // Nothing selected, so rating unchanged + return; + + SetUserrating(userrating); +} + +void CGUIDialogSongInfo::ShowFor(CFileItem* pItem) +{ + if (pItem->m_bIsFolder) + return; + if (!pItem->IsMusicDb()) + pItem->LoadMusicTag(); + if (!pItem->HasMusicInfoTag()) + return; + + CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager(). + GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO); + if (dialog) + { + if (dialog->SetSong(pItem)) // Fetch full song info asynchronously + { + dialog->Open(); + if (dialog->HasUpdatedUserrating()) + { + auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV); + if (window) + window->RefreshContent("songs"); + } + } + } +} + +void CGUIDialogSongInfo::OnPlaySong(const std::shared_ptr<CFileItem>& item) +{ + Close(true); + MUSIC_UTILS::PlayItem(item); +} diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.h b/xbmc/music/dialogs/GUIDialogSongInfo.h new file mode 100644 index 0000000..274f1ac --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogSongInfo.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "FileItem.h" +#include "guilib/GUIDialog.h" +#include "threads/Event.h" + +#include <memory> + +class CGUIDialogSongInfo : + public CGUIDialog +{ +public: + CGUIDialogSongInfo(void); + ~CGUIDialogSongInfo(void) override; + bool OnMessage(CGUIMessage& message) override; + bool SetSong(CFileItem* item); + void SetArtTypeList(CFileItemList& artlist); + bool OnAction(const CAction& action) override; + bool OnBack(int actionID) override; + bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; } + + bool HasListItems() const override { return true; } + CFileItemPtr GetCurrentListItem(int offset = 0) override; + std::string GetContent(); + //const CFileItemList& CurrentDirectory() const { return m_artTypeList; } + bool IsCancelled() const { return m_cancelled; } + void FetchComplete(); + + static void ShowFor(CFileItem* pItem); +protected: + void OnInitWindow() override; + void Update(); + void OnGetArt(); + void SetUserrating(int userrating); + void OnSetUserrating(); + void OnPlaySong(const std::shared_ptr<CFileItem>& item); + + CFileItemPtr m_song; + CFileItemList m_artTypeList; + CEvent m_event; + int m_startUserrating; + bool m_cancelled; + bool m_hasUpdatedUserrating; + long m_albumId = -1; + +}; diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp new file mode 100644 index 0000000..b5f2607 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogVisualisationPresetList.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIVisualisationControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +CGUIDialogVisualisationPresetList::CGUIDialogVisualisationPresetList() + : CGUIDialogSelect(WINDOW_DIALOG_VIS_PRESET_LIST) +{ + m_loadType = KEEP_IN_MEMORY; +} + +bool CGUIDialogVisualisationPresetList::OnMessage(CGUIMessage &message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_VISUALISATION_UNLOADING: + ClearVisualisation(); + break; + } + return CGUIDialogSelect::OnMessage(message); +} + +void CGUIDialogVisualisationPresetList::OnSelect(int idx) +{ + if (m_viz) + m_viz->SetPreset(idx); +} + +void CGUIDialogVisualisationPresetList::ClearVisualisation() +{ + m_viz = nullptr; + Reset(); +} + +void CGUIDialogVisualisationPresetList::SetVisualisation(CGUIVisualisationControl* vis) +{ + m_viz = vis; + Reset(); + if (!m_viz) + { // No viz, but show something if this dialog activated + SetHeading(CVariant{ 10122 }); + CFileItem item(g_localizeStrings.Get(13389)); + Add(item); + } + else + { + SetUseDetails(false); + SetMultiSelection(false); + SetHeading(CVariant{StringUtils::Format(g_localizeStrings.Get(13407), m_viz->Name())}); + std::vector<std::string> presets; + if (m_viz->GetPresetList(presets)) + { + for (const auto& preset : presets) + { + CFileItem item(preset); + item.RemoveExtension(); + Add(item); + } + SetSelected(m_viz->GetActivePreset()); + } + else + { // Viz does not have any presets + // "There are no presets available for this visualisation" + CFileItem item(g_localizeStrings.Get(13389)); + Add(item); + } + } +} + +void CGUIDialogVisualisationPresetList::OnInitWindow() +{ + CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + SetVisualisation(static_cast<CGUIVisualisationControl*>(msg.GetPointer())); + CGUIDialogSelect::OnInitWindow(); +} + +void CGUIDialogVisualisationPresetList::OnDeinitWindow(int nextWindowID) +{ + ClearVisualisation(); + CGUIDialogSelect::OnDeinitWindow(nextWindowID); +} diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h new file mode 100644 index 0000000..c832163 --- /dev/null +++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "dialogs/GUIDialogSelect.h" +#include "guilib/GUIDialog.h" + +class CGUIVisualisationControl; +class CFileItemList; + +class CGUIDialogVisualisationPresetList : public CGUIDialogSelect +{ +public: + CGUIDialogVisualisationPresetList(); + bool OnMessage(CGUIMessage &message) override; + +protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + void OnSelect(int idx) override; + +private: + void ClearVisualisation(); + void SetVisualisation(CGUIVisualisationControl *addon); + CGUIVisualisationControl* m_viz = nullptr; +}; diff --git a/xbmc/music/infoscanner/CMakeLists.txt b/xbmc/music/infoscanner/CMakeLists.txt new file mode 100644 index 0000000..b8c24a0 --- /dev/null +++ b/xbmc/music/infoscanner/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES MusicAlbumInfo.cpp + MusicArtistInfo.cpp + MusicInfoScanner.cpp + MusicInfoScraper.cpp) + +set(HEADERS MusicAlbumInfo.h + MusicArtistInfo.h + MusicInfoScanner.h + MusicInfoScraper.h) + +core_add_library(music_infoscanner) diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.cpp b/xbmc/music/infoscanner/MusicAlbumInfo.cpp new file mode 100644 index 0000000..90aedd3 --- /dev/null +++ b/xbmc/music/infoscanner/MusicAlbumInfo.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicAlbumInfo.h" + +#include "addons/Scraper.h" +#include "settings/AdvancedSettings.h" +#include "utils/StringUtils.h" + +using namespace MUSIC_GRABBER; + +CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL) + : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL) +{ + m_relevance = -1; + m_bLoaded = false; +} + +CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbum, + const std::string& strArtist, + const std::string& strAlbumInfo, + const CScraperUrl& strAlbumURL) + : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL) +{ + m_album.strAlbum = strAlbum; + //Just setting artist desc, not populating album artist credits. + m_album.strArtistDesc = strArtist; + m_relevance = -1; + m_bLoaded = false; +} + +void CMusicAlbumInfo::SetAlbum(CAlbum& album) +{ + m_album = album; + m_strTitle2 = ""; + m_bLoaded = true; +} + +bool CMusicAlbumInfo::Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper) +{ + bool fSuccess = scraper->GetAlbumDetails(http, m_albumURL, m_album); + if (fSuccess && m_strTitle2.empty()) + m_strTitle2 = m_album.strAlbum; + SetLoaded(fSuccess); + return fSuccess; +} + diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.h b/xbmc/music/infoscanner/MusicAlbumInfo.h new file mode 100644 index 0000000..91b8d5d --- /dev/null +++ b/xbmc/music/infoscanner/MusicAlbumInfo.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/Scraper.h" +#include "music/Album.h" +#include "utils/ScraperUrl.h" + +class CXBMCTinyXML; + +namespace XFILE { class CCurlFile; } + +namespace MUSIC_GRABBER +{ +class CMusicAlbumInfo +{ +public: + CMusicAlbumInfo() = default; + CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL); + CMusicAlbumInfo(const std::string& strAlbum, const std::string& strArtist, const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL); + virtual ~CMusicAlbumInfo() = default; + + bool Loaded() const { return m_bLoaded; } + void SetLoaded(bool bLoaded) { m_bLoaded = bLoaded; } + const CAlbum &GetAlbum() const { return m_album; } + CAlbum& GetAlbum() { return m_album; } + void SetAlbum(CAlbum& album); + const std::string& GetTitle2() const { return m_strTitle2; } + void SetTitle(const std::string& strTitle) { m_album.strAlbum = strTitle; } + const CScraperUrl& GetAlbumURL() const { return m_albumURL; } + float GetRelevance() const { return m_relevance; } + void SetRelevance(float relevance) { m_relevance = relevance; } + + bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper); + +protected: + bool m_bLoaded = false; + CAlbum m_album; + float m_relevance = -1; + std::string m_strTitle2; + CScraperUrl m_albumURL; +}; + +} diff --git a/xbmc/music/infoscanner/MusicArtistInfo.cpp b/xbmc/music/infoscanner/MusicArtistInfo.cpp new file mode 100644 index 0000000..3a3f64a --- /dev/null +++ b/xbmc/music/infoscanner/MusicArtistInfo.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicArtistInfo.h" + +#include "addons/Scraper.h" + +using namespace XFILE; +using namespace MUSIC_GRABBER; + +CMusicArtistInfo::CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL): + m_artistURL(strArtistURL) +{ + m_artist.strArtist = strArtist; + m_bLoaded = false; +} + +void CMusicArtistInfo::SetArtist(const CArtist& artist) +{ + m_artist = artist; + m_bLoaded = true; +} + +bool CMusicArtistInfo::Load(CCurlFile& http, const ADDON::ScraperPtr& scraper, + const std::string &strSearch) +{ + return m_bLoaded = scraper->GetArtistDetails(http, m_artistURL, strSearch, m_artist); +} + diff --git a/xbmc/music/infoscanner/MusicArtistInfo.h b/xbmc/music/infoscanner/MusicArtistInfo.h new file mode 100644 index 0000000..1817589 --- /dev/null +++ b/xbmc/music/infoscanner/MusicArtistInfo.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/Scraper.h" +#include "music/Artist.h" + +class CXBMCTinyXML; +class CScraperUrl; + +namespace MUSIC_GRABBER +{ +class CMusicArtistInfo +{ +public: + CMusicArtistInfo() = default; + CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL); + virtual ~CMusicArtistInfo() = default; + bool Loaded() const { return m_bLoaded; } + void SetLoaded() { m_bLoaded = true; } + void SetArtist(const CArtist& artist); + const CArtist& GetArtist() const { return m_artist; } + CArtist& GetArtist() { return m_artist; } + const CScraperUrl& GetArtistURL() const { return m_artistURL; } + bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper, + const std::string &strSearch); + +protected: + CArtist m_artist; + CScraperUrl m_artistURL; + bool m_bLoaded = false; +}; +} diff --git a/xbmc/music/infoscanner/MusicInfoScanner.cpp b/xbmc/music/infoscanner/MusicInfoScanner.cpp new file mode 100644 index 0000000..f828b44 --- /dev/null +++ b/xbmc/music/infoscanner/MusicInfoScanner.cpp @@ -0,0 +1,2338 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoScanner.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "GUIUserMessages.h" +#include "MusicAlbumInfo.h" +#include "MusicInfoScraper.h" +#include "NfoFile.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "Util.h" +#include "addons/AddonSystemSettings.h" +#include "addons/Scraper.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "events/EventLog.h" +#include "events/MediaLibraryEvent.h" +#include "filesystem/Directory.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h" +#include "filesystem/SmartPlaylistDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "interfaces/AnnouncementManager.h" +#include "music/MusicLibraryQueue.h" +#include "music/MusicThumbLoader.h" +#include "music/MusicUtils.h" +#include "music/tags/MusicInfoTag.h" +#include "music/tags/MusicInfoTagLoaderFactory.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Digest.h" +#include "utils/FileExtensionProvider.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <utility> + +using namespace MUSIC_INFO; +using namespace XFILE; +using namespace MUSICDATABASEDIRECTORY; +using namespace MUSIC_GRABBER; +using namespace ADDON; +using KODI::UTILITY::CDigest; + +CMusicInfoScanner::CMusicInfoScanner() +: m_fileCountReader(this, "MusicFileCounter") +{ + m_bStop = false; + m_currentItem=0; + m_itemCount=0; + m_flags = 0; +} + +CMusicInfoScanner::~CMusicInfoScanner() = default; + +void CMusicInfoScanner::Process() +{ + m_bStop = false; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanStarted"); + try + { + if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE)) + { + CGUIDialogExtendedProgressBar* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS); + if (dialog) + m_handle = dialog->GetHandle(g_localizeStrings.Get(314)); + } + + // check if we only need to perform a cleaning + if (m_bClean && m_pathsToScan.empty()) + { + CMusicLibraryQueue::GetInstance().CleanLibrary(false); + m_handle = NULL; + m_bRunning = false; + + return; + } + + auto tick = std::chrono::steady_clock::now(); + m_musicDatabase.Open(); + m_bCanInterrupt = true; + + if (m_scanType == 0) // load info from files + { + CLog::Log(LOGDEBUG, "{} - Starting scan", __FUNCTION__); + + if (m_handle) + m_handle->SetTitle(g_localizeStrings.Get(505)); + + // Reset progress vars + m_currentItem=0; + m_itemCount=-1; + + // Create the thread to count all files to be scanned + if (m_handle) + m_fileCountReader.Create(); + + // Database operations should not be canceled + // using Interrupt() while scanning as it could + // result in unexpected behaviour. + m_bCanInterrupt = false; + m_needsCleanup = false; + + bool commit = true; + for (const auto& it : m_pathsToScan) + { + if (!CDirectory::Exists(it) && !m_bClean) + { + /* + * Note that this will skip scanning (if m_bClean is disabled) if the directory really + * doesn't exist. Since the music scanner is fed with a list of existing paths from the DB + * and cleans out all songs under that path as its first step before re-adding files, if + * the entire source is offline we totally empty the music database in one go. + */ + CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan.", __FUNCTION__, + it); + m_seenPaths.insert(it); + continue; + } + + // Clear list of albums added by this scan + m_albumsAdded.clear(); + bool scancomplete = DoScan(it); + if (scancomplete) + { + if (m_albumsAdded.size() > 0) + { + // Set local art for added album disc sets and primary album artists + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) != + CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE) + RetrieveLocalArt(); + + if (m_flags & SCAN_ONLINE) + // Download additional album and artist information for the recently added albums. + // This also identifies any local artist art if it exists, and gives it priority, + // otherwise it is set to the first available from the remote art that was scraped. + ScrapeInfoAddedAlbums(); + } + } + else + { + commit = false; + break; + } + } + + if (commit) + { + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools(); + + if (m_needsCleanup) + { + if (m_handle) + { + m_handle->SetTitle(g_localizeStrings.Get(700)); + m_handle->SetText(""); + } + + m_musicDatabase.CleanupOrphanedItems(); + m_musicDatabase.CheckArtistLinksChanged(); + + if (m_handle) + m_handle->SetTitle(g_localizeStrings.Get(331)); + + m_musicDatabase.Compress(false); + } + } + + m_fileCountReader.StopThread(); + + m_musicDatabase.EmptyCache(); + + auto elapsed = + std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - tick); + CLog::Log(LOGINFO, + "My Music: Scanning for music info using worker thread, operation took {}s", + elapsed.count()); + } + if (m_scanType == 1) // load album info + { + for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it) + { + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(*it, params); + // Only scrape information for albums that have not been scraped before + // For refresh of information the lastscraped date is optionally clearered elsewhere + if (m_musicDatabase.HasAlbumBeenScraped(params.GetAlbumId())) + continue; + + CAlbum album; + m_musicDatabase.GetAlbum(params.GetAlbumId(), album); + if (m_handle) + { + float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size()); + m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum); + m_handle->SetPercentage(percentage); + } + + // find album info + ADDON::ScraperPtr scraper; + if (!m_musicDatabase.GetScraper(album.idAlbum, CONTENT_ALBUMS, scraper)) + continue; + + UpdateDatabaseAlbumInfo(album, scraper, false); + + if (m_bStop) + break; + } + } + if (m_scanType == 2) // load artist info + { + for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it) + { + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(*it, params); + // Only scrape information for artists that have not been scraped before + // For refresh of information the lastscraped date is optionally clearered elsewhere + if (m_musicDatabase.HasArtistBeenScraped(params.GetArtistId())) + continue; + + CArtist artist; + m_musicDatabase.GetArtist(params.GetArtistId(), artist); + m_musicDatabase.GetArtistPath(artist, artist.strPath); + + if (m_handle) + { + float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size()); + m_handle->SetText(artist.strArtist); + m_handle->SetPercentage(percentage); + } + + // find album info + ADDON::ScraperPtr scraper; + if (!m_musicDatabase.GetScraper(artist.idArtist, CONTENT_ARTISTS, scraper) || !scraper) + continue; + + UpdateDatabaseArtistInfo(artist, scraper, false); + + if (m_bStop) + break; + } + } + //propagate artist sort names to albums and songs + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryArtistSortOnUpdate) + m_musicDatabase.UpdateArtistSortNames(); + } + catch (...) + { + CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning."); + } + m_musicDatabase.Close(); + CLog::Log(LOGDEBUG, "{} - Finished scan", __FUNCTION__); + + m_bRunning = false; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanFinished"); + + // we need to clear the musicdb cache and update any active lists + CUtil::DeleteMusicDatabaseDirectoryCache(); + CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); + + if (m_handle) + m_handle->MarkFinished(); + m_handle = NULL; +} + +void CMusicInfoScanner::Start(const std::string& strDirectory, int flags) +{ + m_fileCountReader.StopThread(); + + m_pathsToScan.clear(); + m_seenPaths.clear(); + m_albumsAdded.clear(); + m_flags = flags; + + m_musicDatabase.Open(); + // Check db sources match xml file and update if they don't + m_musicDatabase.UpdateSources(); + + if (strDirectory.empty()) + { // Scan all paths in the database. We do this by scanning all paths in the + // db, and crossing them off the list as we go. + m_musicDatabase.GetPaths(m_pathsToScan); + m_idSourcePath = -1; + } + else + { + m_pathsToScan.insert(strDirectory); + m_idSourcePath = m_musicDatabase.GetSourceFromPath(strDirectory); + } + m_musicDatabase.Close(); + + m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryCleanOnUpdate; + + m_scanType = 0; + m_bRunning = true; + Process(); +} + +void CMusicInfoScanner::FetchAlbumInfo(const std::string& strDirectory, + bool refresh) +{ + m_fileCountReader.StopThread(); + m_pathsToScan.clear(); + + CFileItemList items; + if (strDirectory.empty()) + { + m_musicDatabase.Open(); + m_musicDatabase.GetAlbumsNav("musicdb://albums/", items); + m_musicDatabase.Close(); + } + else + { + CURL pathToUrl(strDirectory); + + if (pathToUrl.IsProtocol("musicdb")) + { + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(strDirectory, params); + if (params.GetAlbumId() != -1) + { + //Add single album (id and path) as item to scan + CFileItemPtr item(new CFileItem(strDirectory, false)); + item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum); + items.Add(item); + } + else + { + CMusicDatabaseDirectory dir; + NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory); + if (childtype == NODE_TYPE_ALBUM) + dir.GetDirectory(pathToUrl, items); + } + } + else if (StringUtils::EndsWith(strDirectory, ".xsp")) + { + CSmartPlaylistDirectory dir; + dir.GetDirectory(pathToUrl, items); + } + } + + m_musicDatabase.Open(); + for (int i=0;i<items.Size();++i) + { + if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder()) + continue; + + m_pathsToScan.insert(items[i]->GetPath()); + if (refresh) + { + m_musicDatabase.ClearAlbumLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId()); + } + } + m_musicDatabase.Close(); + + m_scanType = 1; + m_bRunning = true; + Process(); +} + +void CMusicInfoScanner::FetchArtistInfo(const std::string& strDirectory, + bool refresh) +{ + m_fileCountReader.StopThread(); + m_pathsToScan.clear(); + CFileItemList items; + + if (strDirectory.empty()) + { + m_musicDatabase.Open(); + m_musicDatabase.GetArtistsNav("musicdb://artists/", items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1); + m_musicDatabase.Close(); + } + else + { + CURL pathToUrl(strDirectory); + + if (pathToUrl.IsProtocol("musicdb")) + { + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(strDirectory, params); + if (params.GetArtistId() != -1) + { + //Add single artist (id and path) as item to scan + CFileItemPtr item(new CFileItem(strDirectory, false)); + item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeArtist); + items.Add(item); + } + else + { + CMusicDatabaseDirectory dir; + NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory); + if (childtype == NODE_TYPE_ARTIST) + dir.GetDirectory(pathToUrl, items); + } + } + else if (StringUtils::EndsWith(strDirectory, ".xsp")) + { + CSmartPlaylistDirectory dir; + dir.GetDirectory(pathToUrl, items); + } + } + + m_musicDatabase.Open(); + for (int i=0;i<items.Size();++i) + { + if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder()) + continue; + + m_pathsToScan.insert(items[i]->GetPath()); + if (refresh) + { + m_musicDatabase.ClearArtistLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId()); + } + } + m_musicDatabase.Close(); + + m_scanType = 2; + m_bRunning = true; + Process(); +} + +void CMusicInfoScanner::Stop() +{ + if (m_bCanInterrupt) + m_musicDatabase.Interrupt(); + + m_bStop = true; +} + +static void OnDirectoryScanned(const std::string& strDirectory) +{ + CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0); + msg.SetStringParam(strDirectory); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +static std::string Prettify(const std::string& strDirectory) +{ + CURL url(strDirectory); + + return CURL::Decode(url.GetWithoutUserDetails()); +} + +bool CMusicInfoScanner::DoScan(const std::string& strDirectory) +{ + if (m_handle) + { + m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..." + m_handle->SetText(Prettify(strDirectory)); + } + + std::set<std::string>::const_iterator it = m_seenPaths.find(strDirectory); + if (it != m_seenPaths.end()) + return true; + + m_seenPaths.insert(strDirectory); + + // Discard all excluded files defined by m_musicExcludeRegExps + const std::vector<std::string> ®exps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps; + + if (CUtil::ExcludeFileOrFolder(strDirectory, regexps)) + return true; + + if (HasNoMedia(strDirectory)) + return true; + + // load subfolder + CFileItemList items; + CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|.jpg|.tbn|.lrc|.cdg", DIR_FLAG_DEFAULTS); + + // sort and get the path hash. Note that we don't filter .cue sheet items here as we want + // to detect changes in the .cue sheet as well. The .cue sheet items only need filtering + // if we have a changed hash. + items.Sort(SortByLabel, SortOrderAscending); + std::string hash; + GetPathHash(items, hash); + + // check whether we need to rescan or not + std::string dbHash; + if ((m_flags & SCAN_RESCAN) || !m_musicDatabase.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash)) + { // path has changed - rescan + if (dbHash.empty()) + CLog::Log(LOGDEBUG, "{} Scanning dir '{}' as not in the database", __FUNCTION__, + CURL::GetRedacted(strDirectory)); + else + CLog::Log(LOGDEBUG, "{} Rescanning dir '{}' due to change", __FUNCTION__, + CURL::GetRedacted(strDirectory)); + + if (m_handle) + m_handle->SetTitle(g_localizeStrings.Get(505)); //"Loading media information from files..." + + // filter items in the sub dir (for .cue sheet support) + items.FilterCueItems(); + items.Sort(SortByLabel, SortOrderAscending); + + // and then scan in the new information from tags + if (RetrieveMusicInfo(strDirectory, items) > 0) + { + if (m_handle) + OnDirectoryScanned(strDirectory); + } + + // save information about this folder + m_musicDatabase.SetPathHash(strDirectory, hash); + } + else + { // path is the same - no need to rescan + CLog::Log(LOGDEBUG, "{} Skipping dir '{}' due to no change", __FUNCTION__, + CURL::GetRedacted(strDirectory)); + m_currentItem += CountFiles(items, false); // false for non-recursive + + // updated the dialog with our progress + if (m_handle) + { + if (m_itemCount>0) + m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount)); + OnDirectoryScanned(strDirectory); + } + } + + // now scan the subfolders + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr pItem = items[i]; + + if (m_bStop) + break; + // if we have a directory item (non-playlist) we then recurse into that folder + if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList()) + { + std::string strPath=pItem->GetPath(); + if (!DoScan(strPath)) + { + m_bStop = true; + } + } + } + return !m_bStop; +} + +CInfoScanner::INFO_RET CMusicInfoScanner::ScanTags(const CFileItemList& items, + CFileItemList& scannedItems) +{ + std::vector<std::string> regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps; + + for (int i = 0; i < items.Size(); ++i) + { + if (m_bStop) + return INFO_CANCELLED; + + CFileItemPtr pItem = items[i]; + + if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps)) + continue; + + if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics()) + continue; + + m_currentItem++; + + CMusicInfoTag& tag = *pItem->GetMusicInfoTag(); + if (!tag.Loaded()) + { + std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem)); + if (nullptr != pLoader) + pLoader->Load(pItem->GetPath(), tag); + } + + if (m_handle && m_itemCount>0) + m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount)); + + if (!tag.Loaded() && !pItem->HasCueDocument()) + { + CLog::Log(LOGDEBUG, "{} - No tag found for: {}", __FUNCTION__, pItem->GetPath()); + continue; + } + else + { + if (!tag.GetCueSheet().empty()) + pItem->LoadEmbeddedCue(); + } + + if (pItem->HasCueDocument()) + pItem->LoadTracksFromCueDocument(scannedItems); + else + scannedItems.Add(pItem); + } + return INFO_ADDED; +} + +static bool SortSongsByTrack(const CSong& song, const CSong& song2) +{ + return song.iTrack < song2.iTrack; +} + +void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */) +{ + /* + * Step 1: Convert the FileItems into Songs. + * If they're MB tagged, create albums directly from the FileItems. + * If they're non-MB tagged, index them by album name ready for step 2. + */ + std::map<std::string, VECSONGS> songsByAlbumNames; + for (int i = 0; i < items.Size(); ++i) + { + CMusicInfoTag& tag = *items[i]->GetMusicInfoTag(); + CSong song(*items[i]); + + // keep the db-only fields intact on rescan... + if (songsMap != NULL) + { + // Match up item to songs in library previously scanned with this path + MAPSONGS::iterator songlist = songsMap->find(items[i]->GetPath()); + if (songlist != songsMap->end()) + { + VECSONGS::iterator foundsong; + if (songlist->second.size() == 1) + foundsong = songlist->second.begin(); + else + { + // When filename mapped to multiple songs it is from cuesheet, match on disc/track number + int disctrack = tag.GetTrackAndDiscNumber(); + foundsong = std::find_if(songlist->second.begin(), songlist->second.end(), + [&](const CSong& song) { return disctrack == song.iTrack; }); + } + if (foundsong != songlist->second.end()) + { + song.idSong = foundsong->idSong; // Reuse ID + song.dateNew = foundsong->dateNew; // Keep date originally created + song.iTimesPlayed = foundsong->iTimesPlayed; + song.lastPlayed = foundsong->lastPlayed; + if (song.rating == 0) + song.rating = foundsong->rating; + if (song.userrating == 0) + song.userrating = foundsong->userrating; + if (song.strThumb.empty()) + song.strThumb = foundsong->strThumb; + } + } + } + + if (!tag.GetMusicBrainzAlbumID().empty()) + { + VECALBUMS::iterator it; + for (it = albums.begin(); it != albums.end(); ++it) + if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID()) + break; + + if (it == albums.end()) + { + CAlbum album(*items[i]); + album.songs.push_back(song); + albums.push_back(album); + } + else + it->songs.push_back(song); + } + else + songsByAlbumNames[tag.GetAlbum()].push_back(song); + } + + /* + Step 2: Split into unique albums based on album name and album artist + In the case where the album artist is unknown, we use the primary artist + (i.e. first artist from each song). + */ + for (auto& songsByAlbumName : songsByAlbumNames) + { + VECSONGS& songs = songsByAlbumName.second; + // sort the songs by tracknumber to identify duplicate track numbers + sort(songs.begin(), songs.end(), SortSongsByTrack); + + // map the songs to their primary artists + bool tracksOverlap = false; + bool hasAlbumArtist = false; + bool isCompilation = true; + std::string old_DiscSubtitle; + + std::map<std::string, std::vector<CSong *> > artists; + for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song) + { + // test for song overlap + if (song != songs.begin() && song->iTrack == (song - 1)->iTrack) + tracksOverlap = true; + + if (!song->bCompilation) + isCompilation = false; + + if (song->strDiscSubtitle != old_DiscSubtitle) + old_DiscSubtitle = song->strDiscSubtitle; + + // get primary artist + std::string primary; + if (!song->GetAlbumArtist().empty()) + { + primary = song->GetAlbumArtist()[0]; + hasAlbumArtist = true; + } + else if (!song->artistCredits.empty()) + primary = song->artistCredits.begin()->GetArtist(); + + // add to the artist map + artists[primary].push_back(&(*song)); + } + + /* + We have a Various Artists compilation if + 1. album name is non-empty AND + 2a. no tracks overlap OR + 2b. all tracks are marked as part of compilation AND + 3a. a unique primary artist is specified as "various", "various artists" or the localized value + OR + 3b. we have at least two primary artists and no album artist specified. + */ + std::string various = g_localizeStrings.Get(340); // Various Artists + bool compilation = + !songsByAlbumName.first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a + if (artists.size() == 1) + { + std::string artist = artists.begin()->first; StringUtils::ToLower(artist); + if (!StringUtils::EqualsNoCase(artist, "various") && + !StringUtils::EqualsNoCase(artist, "various artists") && + !StringUtils::EqualsNoCase(artist, various)) // 3a + compilation = false; + else + // Grab name for use in "various artist" artist + various = artists.begin()->first; + } + else if (hasAlbumArtist) // 3b + compilation = false; + + // Such a compilation album is stored under a unique artist that matches on Musicbrainz ID + // the "various artists" artist for music tagged with mbids. + if (compilation) + { + CLog::Log(LOGDEBUG, "Album '{}' is a compilation as there's no overlapping tracks and {}", + songsByAlbumName.first, + hasAlbumArtist ? "the album artist is 'Various'" + : "there is more than one unique artist"); + // Clear song artists from artists map, put songs under "various artists" mbid entry + artists.clear(); + for (auto& song : songs) + artists[VARIOUSARTISTS_MBID].push_back(&song); + } + + /* + We also have a compilation album when album name is non-empty and ALL tracks are marked as part of + a compilation even if an album artist is given, or all songs have the same primary artist. For + example an anthology - a collection of recordings from various old sources + combined together such as a "best of", retrospective or rarities type release. + + Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b. + The album artist can be determined just like any normal album. + */ + if (!compilation && !songsByAlbumName.first.empty() && isCompilation) + { + compilation = true; + CLog::Log(LOGDEBUG, + "Album '{}' is a compilation as all songs are marked as part of a compilation", + songsByAlbumName.first); + } + + /* + Step 3: Find the common albumartist for each song and assign + albumartist to those tracks that don't have it set. + */ + for (auto& j : artists) + { + /* Find the common artist(s) for these songs (grouped under primary artist). + Various artist compilations already under the unique "various artists" mbid. + Take from albumartist tag when present, or use artist tag. + When from albumartist tag also check albumartistsort tag and take first non-empty value + */ + std::vector<CSong*>& artistSongs = j.second; + std::vector<std::string> common; + std::string albumartistsort; + if (artistSongs.front()->GetAlbumArtist().empty()) + common = artistSongs.front()->GetArtist(); + else + { + common = artistSongs.front()->GetAlbumArtist(); + albumartistsort = artistSongs.front()->GetAlbumArtistSort(); + } + for (std::vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k) + { + unsigned int match = 0; + std::vector<std::string> compare; + if ((*k)->GetAlbumArtist().empty()) + compare = (*k)->GetArtist(); + else + { + compare = (*k)->GetAlbumArtist(); + if (albumartistsort.empty()) + albumartistsort = (*k)->GetAlbumArtistSort(); + } + for (; match < common.size() && match < compare.size(); match++) + { + if (compare[match] != common[match]) + break; + } + common.erase(common.begin() + match, common.end()); + } + if (j.first == VARIOUSARTISTS_MBID) + { + common.clear(); + common.emplace_back(VARIOUSARTISTS_MBID); + } + + /* + Step 4: Assign the album artist for each song that doesn't have it set + and add to the album vector + */ + CAlbum album; + album.strAlbum = songsByAlbumName.first; + + //Split the albumartist sort string to try and get sort names for individual artists + std::vector<std::string> sortnames = StringUtils::Split(albumartistsort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + if (sortnames.size() != common.size()) + // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader + sortnames = StringUtils::SplitMulti(sortnames, { ";", ":", "|", "#" }); + + for (size_t i = 0; i < common.size(); i++) + { + if (common[i] == VARIOUSARTISTS_MBID) + /* Treat "various", "various artists" and the localized equivalent name as the same + album artist as the artist with Musicbrainz ID 89ad4ac3-39f7-470e-963a-56509c546377. + If adding this artist for the first time then the name will be set to either the primary + artist read from tags when 3a, or the localized value for "various artists" when not 3a. + This means that tag values are no longer translated into the current language. + */ + album.artistCredits.emplace_back(various, VARIOUSARTISTS_MBID); + else + { + album.artistCredits.emplace_back(StringUtils::Trim(common[i])); + // Set artist sort name providing we have as many as we have artists, + // otherwise something is wrong with them so ignore rather than guess. + if (sortnames.size() == common.size()) + album.artistCredits.back().SetSortName(StringUtils::Trim(sortnames[i])); + } + } + album.bCompilation = compilation; + for (auto& k : artistSongs) + { + if (k->GetAlbumArtist().empty()) + k->SetAlbumArtist(common); + //! @todo in future we may wish to union up the genres, for now we assume they're the same + album.genre = k->genre; + album.strArtistSort = k->GetAlbumArtistSort(); + // in addition, we may want to use release date as discriminating between albums + album.strReleaseDate = k->strReleaseDate, + album.strLabel = k->strRecordLabel; + album.strType = k->strAlbumType; + album.songs.push_back(*k); + } + albums.push_back(album); + } + } +} + +CInfoScanner::INFO_RET +CMusicInfoScanner::UpdateAlbumInfo(CAlbum& album, + const ADDON::ScraperPtr& scraper, + bool bAllowSelection, + CGUIDialogProgress* pDialog) +{ + m_musicDatabase.Open(); + INFO_RET result = UpdateDatabaseAlbumInfo(album, scraper, bAllowSelection, pDialog); + m_musicDatabase.Close(); + return result; +} + +CInfoScanner::INFO_RET +CMusicInfoScanner::UpdateArtistInfo(CArtist& artist, + const ADDON::ScraperPtr& scraper, + bool bAllowSelection, + CGUIDialogProgress* pDialog) +{ + m_musicDatabase.Open(); + INFO_RET result = UpdateDatabaseArtistInfo(artist, scraper, bAllowSelection, pDialog); + m_musicDatabase.Close(); + return result; +} + +int CMusicInfoScanner::RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items) +{ + MAPSONGS songsMap; + + // get all information for all files in current directory from database, and remove them + if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap)) + m_needsCleanup = true; + + CFileItemList scannedItems; + if (ScanTags(items, scannedItems) == INFO_CANCELLED || scannedItems.Size() == 0) + return 0; + + VECALBUMS albums; + FileItemsToAlbums(scannedItems, albums, &songsMap); + + /* + Set thumb for songs and, if only one album in folder, store the thumb for + the album (music db) and the folder path (in Textures db) too. + The album and path thumb is either set to the folder art, or failing that to + the art embedded in the first music file. + Song thumb is only set when it varies, otherwise it is cleared so that it will + fallback to the album art (that may be from the first file, or that of the + folder or set later by scraping from NFO files or remote sources). Clearing + saves caching repeats of the same image. + + However even if all songs are from one album this may not be the album + folder. It could be just a subfolder containing some of the songs from a disc + set e.g. CD1, CD2 etc., or the album could spread across many folders. In + this case the album art gets reset every time a folder with songs from just + that album is processed, and needs to be corrected later once all the parts + of the album have been scanned. + */ + FindArtForAlbums(albums, items.GetPath()); + + /* Strategy: Having scanned tags and made a list of albums, add them to the library. Only then try + to scrape additional album and artist information. Music is often tagged to a mixed standard + - some albums have mbid tags, some don't. Once all the music files have been added to the library, + the mbid for an artist will be known even if it was only tagged on one song. The artist is best + scraped with an mbid, so scrape after all the files that may provide that tag have been scanned. + That artist mbid can then be used to improve the accuracy of scraping other albums by that artist + even when it was not in the tagging for that album. + + Doing scraping, generally the slower activity, in the background after scanning has fully populated + the library also means that the user can use their library to select music to play sooner. + */ + + int numAdded = 0; + + // Add all albums to the library, and hence any new song or album artists or other contributors + for (auto& album : albums) + { + if (m_bStop) + break; + + // mark albums without a title as singles + if (album.strAlbum.empty()) + album.releaseType = CAlbum::Single; + + album.strPath = strDirectory; + m_musicDatabase.AddAlbum(album, m_idSourcePath); + m_albumsAdded.insert(album.idAlbum); + + numAdded += static_cast<int>(album.songs.size()); + } + return numAdded; +} + +void MUSIC_INFO::CMusicInfoScanner::ScrapeInfoAddedAlbums() +{ + /* Strategy: Having scanned tags, make a list of albums and add them to the library, only then try + to scrape additional album and artist information. Music is often tagged to a mixed standard + - some albums have mbid tags, some don't. Once all the music files have been added to the library, + the mbid for an artist will be known even if it was only tagged on one song. The artist is best + scraped with an mbid, so scrape after all the files that may provide that tag have been scanned. + That artist mbid can then be used to improve the accuracy of scraping other albums by that artist + even when it was not in the tagging for that album. + + Doing scraping, generally the slower activity, in the background after scanning has fully populated + the library also means that the user can use their library to select music to play sooner. + */ + + /* Scrape additional album and artist data. + For albums and artists without mbids, matching on album-artist pair can + be used to identify artist with greater accuracy than artist name alone. + Artist mbid returned by album scraper is used if we do not already have it. + Hence scrape album then related artists. + */ + ADDON::AddonPtr addon; + + ADDON::ScraperPtr albumScraper; + ADDON::ScraperPtr artistScraper; + if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS, addon)) + albumScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon); + + if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS, + addon)) + artistScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon); + + bool albumartistsonly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS); + + if (!albumScraper || !artistScraper) + return; + + int i = 0; + std::set<int> artists; + for (auto albumId : m_albumsAdded) + { + i++; + if (m_bStop) + break; + // Scrape album data + CAlbum album; + if (!m_musicDatabase.HasAlbumBeenScraped(albumId)) + { + if (m_handle) + { + m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum); + m_handle->SetProgress(i, static_cast<int>(m_albumsAdded.size())); + } + + // Fetch any artist mbids for album artist(s) and song artists when scraping those too. + m_musicDatabase.GetAlbum(albumId, album, !albumartistsonly); + UpdateDatabaseAlbumInfo(album, albumScraper, false); + + // Scrape information for artists that have not been scraped before, avoiding repeating + // unsuccessful attempts for every album and song. + for (const auto &artistCredit : album.artistCredits) + { + if (m_bStop) + break; + + if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) && + artists.find(artistCredit.GetArtistId()) == artists.end()) + { + artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted + CArtist artist; + m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist); + UpdateDatabaseArtistInfo(artist, artistScraper, false); + } + } + // Only scrape song artists if they are being displayed in artists node by default + if (!albumartistsonly) + { + for (auto &song : album.songs) + { + if (m_bStop) + break; + for (const auto &artistCredit : song.artistCredits) + { + if (m_bStop) + break; + + CMusicArtistInfo musicArtistInfo; + if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) && + artists.find(artistCredit.GetArtistId()) == artists.end()) + { + artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted + CArtist artist; + m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist); + UpdateDatabaseArtistInfo(artist, artistScraper, false); + } + } + } + } + } + } +} + +/* + Set thumb for songs and the album(if only one album in folder). + The album thumb is either set to the folder art, or failing that to the art + embedded in the first music file. However this does not allow for there being + other folders with more songs from the album e.g. this was a subfolder CD1 + and there is CD2 etc. yet to be processed + Song thumb is only set when it varies, otherwise it is cleared so that it will + fallback to the album art(that may be from the first file, or that of the + folder or set later by scraping from NFO files or remote sources).Clearing + saves caching repeats of the same image. +*/ +void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const std::string &path) +{ + /* + If there's a single album in the folder, then art can be taken from + the folder art. + */ + std::string albumArt; + if (albums.size() == 1) + { + CFileItem album(path, true); + /* + If we are scanning a directory served over http(s) the root directory for an album will set + IsInternetStream to true which prevents scanning it for art. As we can't reach this point + without having read some tags (and tags are not read from streams) we can safely check for + that case and set the IsHTTPDirectory property to enable scanning for art. + */ + if (StringUtils::StartsWithNoCase(path, "http") && StringUtils::EndsWith(path, "/")) + album.SetProperty("IsHTTPDirectory", true); + albumArt = album.GetUserMusicThumb(true); + if (!albumArt.empty()) + albums[0].art["thumb"] = albumArt; + } + for (auto& album : albums) + { + if (albums.size() != 1) + albumArt = ""; + + /* + Find art that is common across these items + If we find a single art image we treat it as the album art + and discard song art else we use first as album art and + keep everything as song art. + */ + bool singleArt = true; + CSong *art = NULL; + for (auto& song : album.songs) + { + if (song.HasArt()) + { + if (art && !art->ArtMatches(song)) + { + singleArt = false; + break; + } + if (!art) + art = &song; + } + } + + /* + assign the first art found to the album - better than no art at all + */ + + if (art && albumArt.empty()) + { + if (!art->strThumb.empty()) + albumArt = art->strThumb; + else + albumArt = CTextureUtils::GetWrappedImageURL(art->strFileName, "music"); + } + + if (!albumArt.empty()) + album.art["thumb"] = albumArt; + + if (singleArt) + { //if singleArt then we can clear the artwork for all songs + for (auto& k : album.songs) + k.strThumb.clear(); + } + else + { // more than one piece of art was found for these songs, so cache per song + for (auto& k : album.songs) + { + if (k.strThumb.empty() && !k.embeddedArt.Empty()) + k.strThumb = CTextureUtils::GetWrappedImageURL(k.strFileName, "music"); + } + } + } + if (albums.size() == 1 && !albumArt.empty()) + { + // assign to folder thumb as well + CFileItem albumItem(path, true); + CMusicThumbLoader loader; + loader.SetCachedImage(albumItem, "thumb", albumArt); + } +} + +void MUSIC_INFO::CMusicInfoScanner::RetrieveLocalArt() +{ + if (m_handle) + { + m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..." + //!@todo: title = Checking for local art + } + + std::set<int> artistsArtDone; // artists processed to avoid unsuccessful repeats + int count = 0; + for (auto albumId : m_albumsAdded) + { + count++; + if (m_bStop) + break; + CAlbum album; + m_musicDatabase.GetAlbum(albumId, album, false); + if (m_handle) + { + m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum); + m_handle->SetProgress(count, static_cast<int>(m_albumsAdded.size())); + } + + /* + Automatically fetch local art from album folder and any disc sets subfolders + + Providing all songs from an album are are under a unique common album + folder (no songs from other albums) then thumb has been set to local art, + or failing that to embedded art, during scanning by FindArtForAlbums(). + But when songs are also spread over multiple subfolders within it e.g. disc + sets, it will have been set to either the art of the last subfolder that was + processed (if there is any), or from the first song in that subfolder with + embedded art (if there is any). To correct this and find any thumb in the + (common) album folder add "thumb" to those missing. + */ + AddAlbumArtwork(album); + + /* + Local album artist art + + Look in the nominated "Artist Information Folder" for thumbs and fanart. + Failing that, for backward compatibility, fallback to the folder immediately + above the album folder. + It can only fallback if the album has a unique folder, and can only do so + for the first album artist if the album is a collaboration e.g. composer, + conductor, orchestra, or by several pop artists in their own right. + Avoids repeatedly processing the same artist by maintaining a set. + + Adding the album may have added new artists, or provide art for an existing + (song) artist, but does not replace any artwork already set. Hence once art + has been found for an album artist, art is not searched for in other folders. + + It will find art for "various artists", if artwork is located above the + folder containing compilatons. + */ + for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit) + { + if (m_bStop) + break; + int idArtist = artistCredit->GetArtistId(); + if (artistsArtDone.find(idArtist) == artistsArtDone.end()) + { + artistsArtDone.insert(idArtist); // Artist processed + + // Get artist and subfolder within the Artist Information Folder + CArtist artist; + m_musicDatabase.GetArtist(idArtist, artist); + m_musicDatabase.GetArtistPath(artist, artist.strPath); + // Location of local art + std::string artfolder; + if (CDirectory::Exists(artist.strPath)) + // When subfolder exists that is only place we look for local art + artfolder = artist.strPath; + else if (!album.strPath.empty() && artistCredit == album.artistCredits.begin()) + { + // If no individual artist subfolder has been found, for primary + // album artist only look in the folder immediately above the album + // folder. Not using GetOldArtistPath here because may not have not + // have scanned all the albums yet. + artfolder = URIUtils::GetParentPath(album.strPath); + } + AddArtistArtwork(artist, artfolder); + } + } + } +} + +int CMusicInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash) +{ + // Create a hash based on the filenames, filesize and filedate. Also count the number of files + if (0 == items.Size()) return 0; + CDigest digest{CDigest::Type::MD5}; + int count = 0; + for (int i = 0; i < items.Size(); ++i) + { + const CFileItemPtr pItem = items[i]; + digest.Update(pItem->GetPath()); + digest.Update((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize)); + KODI::TIME::FileTime time = pItem->m_dateTime; + digest.Update((unsigned char*)&time, sizeof(KODI::TIME::FileTime)); + if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO()) + count++; + } + hash = digest.Finalize(); + return count; +} + +CInfoScanner::INFO_RET +CMusicInfoScanner::UpdateDatabaseAlbumInfo(CAlbum& album, + const ADDON::ScraperPtr& scraper, + bool bAllowSelection, + CGUIDialogProgress* pDialog /* = NULL */) +{ + if (!scraper) + return INFO_ERROR; + + CMusicAlbumInfo albumInfo; + INFO_RET albumDownloadStatus(INFO_CANCELLED); + std::string origArtist(album.GetAlbumArtistString()); + std::string origAlbum(album.strAlbum); + + bool stop(false); + while (!stop) + { + stop = true; + CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, album.strAlbum); + albumDownloadStatus = DownloadAlbumInfo(album, scraper, albumInfo, !bAllowSelection, pDialog); + if (albumDownloadStatus == INFO_NOT_FOUND) + { + if (pDialog && bAllowSelection) + { + std::string strTempAlbum(album.strAlbum); + if (!CGUIKeyboardFactory::ShowAndGetInput(strTempAlbum, CVariant{ g_localizeStrings.Get(16011) }, false)) + albumDownloadStatus = INFO_CANCELLED; + else + { + std::string strTempArtist(album.GetAlbumArtistString()); + if (!CGUIKeyboardFactory::ShowAndGetInput(strTempArtist, CVariant{ g_localizeStrings.Get(16025) }, false)) + albumDownloadStatus = INFO_CANCELLED; + else + { + album.strAlbum = strTempAlbum; + album.strArtistDesc = strTempArtist; + stop = false; + } + } + } + else + { + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(EventPtr(new CMediaLibraryEvent( + MediaTypeAlbum, album.strPath, 24146, + StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeAlbum, album.strAlbum), + CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType()), + CURL::GetRedacted(album.strPath), EventLevel::Warning))); + } + } + } + + // Restore original album and artist name, possibly changed by manual entry + // to get info but should not change outside merge + album.strAlbum = origAlbum; + album.strArtistDesc = origArtist; + + if (albumDownloadStatus == INFO_ADDED) + { + bool overridetags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS); + // Remove art accidentally set by the Python scraper, it only provides URLs of possible artwork + // Art is selected later applying whitelist and other art preferences + albumInfo.GetAlbum().art.clear(); + album.MergeScrapedAlbum(albumInfo.GetAlbum(), overridetags); + m_musicDatabase.UpdateAlbum(album); + albumInfo.SetLoaded(true); + } + + // Check album art. + // Fill any gaps with local art files or use first available from scraped URL list (when it has + // been successfully scraped) as controlled by whitelist. Do this even when no info added + // (cancelled, not found or error), there may be new local art files. + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) != + CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE && + AddAlbumArtwork(album)) + albumDownloadStatus = INFO_ADDED; // Local art added + + return albumDownloadStatus; +} + +CInfoScanner::INFO_RET +CMusicInfoScanner::UpdateDatabaseArtistInfo(CArtist& artist, + const ADDON::ScraperPtr& scraper, + bool bAllowSelection, + CGUIDialogProgress* pDialog /* = NULL */) +{ + if (!scraper) + return INFO_ERROR; + + CMusicArtistInfo artistInfo; + INFO_RET artistDownloadStatus(INFO_CANCELLED); + std::string origArtist(artist.strArtist); + + bool stop(false); + while (!stop) + { + stop = true; + CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, artist.strArtist); + artistDownloadStatus = DownloadArtistInfo(artist, scraper, artistInfo, !bAllowSelection, pDialog); + if (artistDownloadStatus == INFO_NOT_FOUND) + { + if (pDialog && bAllowSelection) + { + if (!CGUIKeyboardFactory::ShowAndGetInput(artist.strArtist, CVariant{ g_localizeStrings.Get(16025) }, false)) + artistDownloadStatus = INFO_CANCELLED; + else + stop = false; + } + else + { + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(EventPtr(new CMediaLibraryEvent( + MediaTypeArtist, artist.strPath, 24146, + StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeArtist, artist.strArtist), + CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType()), + CURL::GetRedacted(artist.strPath), EventLevel::Warning))); + } + } + } + + // Restore original artist name, possibly changed by manual entry to get info + // but should not change outside merge + artist.strArtist = origArtist; + + if (artistDownloadStatus == INFO_ADDED) + { + artist.MergeScrapedArtist(artistInfo.GetArtist(), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS)); + m_musicDatabase.UpdateArtist(artist); + artistInfo.SetLoaded(); + } + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) == + CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE) + return artistDownloadStatus; + + // Check artist art. + // Fill any gaps with local art files, or use first available from scraped + // list (when it has been successfully scraped). Do this even when no info + // added (cancelled, not found or error), there may be new local art files. + // Get individual artist subfolder within the Artist Information Folder + m_musicDatabase.GetArtistPath(artist, artist.strPath); + // Location of local art + std::string artfolder; + if (CDirectory::Exists(artist.strPath)) + // When subfolder exists that is only place we look for art + artfolder = artist.strPath; + else + { + // Fallback to the old location local to music files (when there is a + // unique folder). If there is no folder for the artist, and *only* the + // artist, this will be blank + m_musicDatabase.GetOldArtistPath(artist.idArtist, artfolder); + } + if (AddArtistArtwork(artist, artfolder)) + artistDownloadStatus = INFO_ADDED; // Local art added + + return artistDownloadStatus; // Added, cancelled or not found +} + +#define THRESHOLD .95f + +CInfoScanner::INFO_RET +CMusicInfoScanner::DownloadAlbumInfo(const CAlbum& album, + const ADDON::ScraperPtr& info, + CMusicAlbumInfo& albumInfo, + bool bUseScrapedMBID, + CGUIDialogProgress* pDialog) +{ + if (m_handle) + { + m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20321), info->Name())); + m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum); + } + + // clear our scraper cache + info->ClearCache(); + + CMusicInfoScraper scraper(info); + bool bMusicBrainz = false; + /* + When the mbid is derived from tags scraping of album information is done directly + using that ID, otherwise the lookup is based on album and artist names and can mis-identify the + album (i.e. classical music has many "Symphony No. 5"). To be able to correct any mistakes a + manual refresh of artist information uses either the mbid if derived from tags or the album + and artist names, not any previously scraped mbid. + */ + if (!album.strMusicBrainzAlbumID.empty() && (!album.bScrapedMBID || bUseScrapedMBID)) + { + CScraperUrl musicBrainzURL; + if (ResolveMusicBrainz(album.strMusicBrainzAlbumID, info, musicBrainzURL)) + { + CMusicAlbumInfo albumNfo("nfo", musicBrainzURL); + scraper.GetAlbums().clear(); + scraper.GetAlbums().push_back(albumNfo); + bMusicBrainz = true; + } + } + + // handle nfo files + bool existsNFO = false; + std::string path = album.strPath; + if (path.empty()) + m_musicDatabase.GetAlbumPath(album.idAlbum, path); + + std::string strNfo = URIUtils::AddFileToFolder(path, "album.nfo"); + CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO; + CNfoFile nfoReader; + existsNFO = CFileUtils::Exists(strNfo); + // When on GUI ask user if they want to ignore nfo and refresh from Internet + if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(10523, 20446)) + { + existsNFO = false; + CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo)); + } + if (existsNFO) + { + CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo)); + result = nfoReader.Create(strNfo, info); + if (result == CInfoScanner::FULL_NFO) + { + CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__); + nfoReader.GetDetails(albumInfo.GetAlbum()); + return INFO_ADDED; + } + else if (result == CInfoScanner::URL_NFO || + result == CInfoScanner::COMBINED_NFO) + { + CScraperUrl scrUrl(nfoReader.ScraperUrl()); + CMusicAlbumInfo albumNfo("nfo",scrUrl); + ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo(); + CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name()); + CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl()); + scraper.SetScraperInfo(nfoReaderScraper); + scraper.GetAlbums().clear(); + scraper.GetAlbums().push_back(albumNfo); + } + else if (result != CInfoScanner::OVERRIDE_NFO) + CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo); + } + + if (!scraper.CheckValidOrFallback(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER))) + { // the current scraper is invalid, as is the default - bail + CLog::Log(LOGERROR, "{} - current and default scrapers are invalid. Pick another one", + __FUNCTION__); + return INFO_ERROR; + } + + if (!scraper.GetAlbumCount()) + { + scraper.FindAlbumInfo(album.strAlbum, album.GetAlbumArtistString()); + + while (!scraper.Completed()) + { + if (m_bStop) + { + scraper.Cancel(); + return INFO_CANCELLED; + } + ScannerWait(1); + } + /* + Finding album using xml scraper may request data from Musicbrainz. + MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server + returns 503 errors for all calls from that IP address. + To stay below the rate-limit threshold wait 1s before proceeding + */ + if (!info->IsPython()) + ScannerWait(1000); + } + + CGUIDialogSelect *pDlg = NULL; + int iSelectedAlbum=0; + if ((result == CInfoScanner::NO_NFO || result == CInfoScanner::OVERRIDE_NFO) + && !bMusicBrainz) + { + iSelectedAlbum = -1; // set negative so that we can detect a failure + if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1) + { + double bestRelevance = 0; + double minRelevance = static_cast<double>(THRESHOLD); + if (pDialog || scraper.GetAlbumCount() > 1) // score the matches + { + //show dialog with all albums found + if (pDialog) + { + pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)}); + pDlg->Reset(); + pDlg->EnableButton(true, 413); // manual + pDlg->SetUseDetails(true); + } + + CFileItemList items; + for (int i = 0; i < scraper.GetAlbumCount(); ++i) + { + CMusicAlbumInfo& info = scraper.GetAlbum(i); + double relevance = static_cast<double>(info.GetRelevance()); + if (relevance < 0) + relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, album.strAlbum, + info.GetAlbum().GetAlbumArtistString(), + album.GetAlbumArtistString()); + + // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches) + // otherwise, perfect matches only + if (relevance >= std::max(minRelevance, bestRelevance)) + { // we auto-select the best of these + bestRelevance = relevance; + iSelectedAlbum = i; + } + if (pDialog) + { + // set the label to [relevance] album - artist + std::string strTemp = StringUtils::Format("[{:0.2f}] {}", relevance, info.GetTitle2()); + CFileItemPtr item(new CFileItem("", false)); + item->SetLabel(strTemp); + + std::string strTemp2; + if (!scraper.GetAlbum(i).GetAlbum().strType.empty()) + strTemp2 += scraper.GetAlbum(i).GetAlbum().strType; + if (!scraper.GetAlbum(i).GetAlbum().strReleaseDate.empty()) + strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseDate; + if (!scraper.GetAlbum(i).GetAlbum().strReleaseStatus.empty()) + strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseStatus; + if (!scraper.GetAlbum(i).GetAlbum().strLabel.empty()) + strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strLabel; + item->SetLabel2(strTemp2); + + item->SetArt(scraper.GetAlbum(i).GetAlbum().art); + + items.Add(item); + } + if (!pDialog && relevance > 0.999) // we're so close, no reason to search further + break; + } + + if (pDialog) + { + pDlg->Sort(false); + pDlg->SetItems(items); + pDlg->Open(); + + // and wait till user selects one + if (pDlg->GetSelectedItem() < 0) + { // none chosen + if (!pDlg->IsButtonPressed()) + return INFO_CANCELLED; + + // manual button pressed + std::string strNewAlbum = album.strAlbum; + if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum, CVariant{g_localizeStrings.Get(16011)}, false)) + return INFO_CANCELLED; + if (strNewAlbum == "") + return INFO_CANCELLED; + + std::string strNewArtist = album.GetAlbumArtistString(); + if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false)) + return INFO_CANCELLED; + + pDialog->SetLine(0, CVariant{strNewAlbum}); + pDialog->SetLine(1, CVariant{strNewArtist}); + pDialog->Progress(); + + CAlbum newAlbum = album; + newAlbum.strAlbum = strNewAlbum; + newAlbum.strArtistDesc = strNewArtist; + + return DownloadAlbumInfo(newAlbum, info, albumInfo, bUseScrapedMBID, pDialog); + } + iSelectedAlbum = pDlg->GetSelectedItem(); + } + } + else + { + CMusicAlbumInfo& info = scraper.GetAlbum(0); + double relevance = static_cast<double>(info.GetRelevance()); + if (relevance < 0) + relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, + album.strAlbum, + info.GetAlbum().GetAlbumArtistString(), + album.GetAlbumArtistString()); + if (relevance < static_cast<double>(THRESHOLD)) + return INFO_NOT_FOUND; + + iSelectedAlbum = 0; + } + } + + if (iSelectedAlbum < 0) + return INFO_NOT_FOUND; + + } + + scraper.LoadAlbumInfo(iSelectedAlbum); + while (!scraper.Completed()) + { + if (m_bStop) + { + scraper.Cancel(); + return INFO_CANCELLED; + } + ScannerWait(1); + } + if (!scraper.Succeeded()) + return INFO_ERROR; + /* + Fetching album details using xml scraper may makes requests for data from Musicbrainz. + MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server + returns 503 errors for all calls from that IP address. + To stay below the rate-limit threshold wait 1s before proceeding incase next action is + to scrape another album or artist + */ + if (!info->IsPython()) + ScannerWait(1000); + + albumInfo = scraper.GetAlbum(iSelectedAlbum); + + if (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO) + nfoReader.GetDetails(albumInfo.GetAlbum(), NULL, true); + + return INFO_ADDED; +} + +CInfoScanner::INFO_RET +CMusicInfoScanner::DownloadArtistInfo(const CArtist& artist, + const ADDON::ScraperPtr& info, + MUSIC_GRABBER::CMusicArtistInfo& artistInfo, + bool bUseScrapedMBID, + CGUIDialogProgress* pDialog) +{ + if (m_handle) + { + m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20320), info->Name())); + m_handle->SetText(artist.strArtist); + } + + // clear our scraper cache + info->ClearCache(); + + CMusicInfoScraper scraper(info); + bool bMusicBrainz = false; + /* + When the mbid is derived from tags scraping of artist information is done directly + using that ID, otherwise the lookup is based on name and can mis-identify the artist + (many have same name). To be able to correct any mistakes a manual refresh of artist + information uses either the mbid if derived from tags or the artist name, not any previously + scraped mbid. + */ + if (!artist.strMusicBrainzArtistID.empty() && (!artist.bScrapedMBID || bUseScrapedMBID)) + { + CScraperUrl musicBrainzURL; + if (ResolveMusicBrainz(artist.strMusicBrainzArtistID, info, musicBrainzURL)) + { + CMusicArtistInfo artistNfo("nfo", musicBrainzURL); + scraper.GetArtists().clear(); + scraper.GetArtists().push_back(artistNfo); + bMusicBrainz = true; + } + } + + // Handle nfo files + CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO; + CNfoFile nfoReader; + std::string strNfo; + std::string path; + bool existsNFO = false; + // First look for nfo in the artists folder, the primary location + path = artist.strPath; + // Get path when don't already have it. + bool artistpathfound = !path.empty(); + if (!artistpathfound) + artistpathfound = m_musicDatabase.GetArtistPath(artist, path); + if (artistpathfound) + { + strNfo = URIUtils::AddFileToFolder(path, "artist.nfo"); + existsNFO = CFileUtils::Exists(strNfo); + } + + // If not there fall back local to music files (historic location for those album artists with a unique folder) + if (!existsNFO) + { + artistpathfound = m_musicDatabase.GetOldArtistPath(artist.idArtist, path); + if (artistpathfound) + { + strNfo = URIUtils::AddFileToFolder(path, "artist.nfo"); + existsNFO = CFileUtils::Exists(strNfo); + } + else + CLog::Log(LOGDEBUG, "{} not have path, nfo file not possible", artist.strArtist); + } + + // When on GUI ask user if they want to ignore nfo and refresh from Internet + if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(21891, 20446)) + { + existsNFO = false; + CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo)); + } + + if (existsNFO) + { + CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo)); + result = nfoReader.Create(strNfo, info); + if (result == CInfoScanner::FULL_NFO) + { + CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__); + nfoReader.GetDetails(artistInfo.GetArtist()); + return INFO_ADDED; + } + else if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO) + { + CScraperUrl scrUrl(nfoReader.ScraperUrl()); + CMusicArtistInfo artistNfo("nfo", scrUrl); + ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo(); + CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name()); + CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl()); + scraper.SetScraperInfo(nfoReaderScraper); + scraper.GetArtists().push_back(artistNfo); + } + else + CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo); + } + + if (!scraper.GetArtistCount()) + { + scraper.FindArtistInfo(artist.strArtist); + + while (!scraper.Completed()) + { + if (m_bStop) + { + scraper.Cancel(); + return INFO_CANCELLED; + } + ScannerWait(1); + } + /* + Finding artist using xml scraper makes a request for data from Musicbrainz. + MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter + the server returns 503 errors for all calls from that IP address. To stay + below the rate-limit threshold wait 1s before proceeding + */ + if (!info->IsPython()) + ScannerWait(1000); + } + + int iSelectedArtist = 0; + if (result == CInfoScanner::NO_NFO && !bMusicBrainz) + { + if (scraper.GetArtistCount() >= 1) + { + // now load the first match + if (pDialog && scraper.GetArtistCount() > 1) + { + // if we found more then 1 album, let user choose one + CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pDlg) + { + pDlg->SetHeading(CVariant{g_localizeStrings.Get(21890)}); + pDlg->Reset(); + pDlg->EnableButton(true, 413); // manual + + for (int i = 0; i < scraper.GetArtistCount(); ++i) + { + // set the label to artist + CFileItem item(scraper.GetArtist(i).GetArtist()); + std::string strTemp = scraper.GetArtist(i).GetArtist().strArtist; + if (!scraper.GetArtist(i).GetArtist().strBorn.empty()) + strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")"; + if (!scraper.GetArtist(i).GetArtist().strDisambiguation.empty()) + strTemp += " - " + scraper.GetArtist(i).GetArtist().strDisambiguation; + if (!scraper.GetArtist(i).GetArtist().genre.empty()) + { + std::string genres = StringUtils::Join(scraper.GetArtist(i).GetArtist().genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + if (!genres.empty()) + strTemp = StringUtils::Format("[{}] {}", genres, strTemp); + } + item.SetLabel(strTemp); + item.m_idepth = i; // use this to hold the index of the album in the scraper + pDlg->Add(item); + } + pDlg->Open(); + + // and wait till user selects one + if (pDlg->GetSelectedItem() < 0) + { // none chosen + if (!pDlg->IsButtonPressed()) + return INFO_CANCELLED; + + // manual button pressed + std::string strNewArtist = artist.strArtist; + if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false)) + return INFO_CANCELLED; + + if (pDialog) + { + pDialog->SetLine(0, CVariant{strNewArtist}); + pDialog->Progress(); + } + + CArtist newArtist; + newArtist.strArtist = strNewArtist; + return DownloadArtistInfo(newArtist, info, artistInfo, bUseScrapedMBID, pDialog); + } + iSelectedArtist = pDlg->GetSelectedFileItem()->m_idepth; + } + } + } + else + return INFO_NOT_FOUND; + } + /* + Fetching artist details using xml scraper makes requests for data from Musicbrainz. + MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server + returns 503 errors for all calls from that IP address. + To stay below the rate-limit threshold wait 1s before proceeding incase next action is + to scrape another album or artist + */ + if (!info->IsPython()) + ScannerWait(1000); + + scraper.LoadArtistInfo(iSelectedArtist, artist.strArtist); + while (!scraper.Completed()) + { + if (m_bStop) + { + scraper.Cancel(); + return INFO_CANCELLED; + } + ScannerWait(1); + } + + if (!scraper.Succeeded()) + return INFO_ERROR; + + artistInfo = scraper.GetArtist(iSelectedArtist); + + if (result == CInfoScanner::COMBINED_NFO) + nfoReader.GetDetails(artistInfo.GetArtist(), NULL, true); + + return INFO_ADDED; +} + +bool CMusicInfoScanner::ResolveMusicBrainz(const std::string &strMusicBrainzID, const ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL) +{ + // We have a MusicBrainz ID + // Get a scraper that can resolve it to a MusicBrainz URL & force our + // search directly to the specific album. + bool bMusicBrainz = false; + try + { + musicBrainzURL = preferredScraper->ResolveIDToUrl(strMusicBrainzID); + } + catch (const ADDON::CScraperError &sce) + { + if (sce.FAborted()) + return false; + } + + if (musicBrainzURL.HasUrls()) + { + CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", preferredScraper->Name()); + CLog::Log(LOGDEBUG, "-- nfo url: {}", musicBrainzURL.GetFirstThumbUrl()); + bMusicBrainz = true; + } + + return bMusicBrainz; +} + +void CMusicInfoScanner::ScannerWait(unsigned int milliseconds) +{ + if (milliseconds > 10) + { + CEvent m_StopEvent; + m_StopEvent.Wait(std::chrono::milliseconds(milliseconds)); + } + else + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); +} + +bool CMusicInfoScanner::AddArtistArtwork(CArtist& artist, const std::string& artfolder) +{ + if (!artist.thumbURL.HasUrls() && artfolder.empty()) + return false; // No local or scraped possible art to process + + if (artist.art.empty()) + m_musicDatabase.GetArtForItem(artist.idArtist, MediaTypeArtist, artist.art); + + std::map<std::string, std::string> addedart; + std::string strArt; + + // Handle thumb separately, can be from multiple confgurable file names + if (artist.art.find("thumb") == artist.art.end()) + { + if (!artfolder.empty()) + { // Local music thumbnail images named by "musiclibrary.musicthumbs" + CFileItem item(artfolder, true); + strArt = item.GetUserMusicThumb(true); + } + if (strArt.empty()) + strArt = CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType("thumb")); + if (!strArt.empty()) + addedart.insert(std::make_pair("thumb", strArt)); + } + + // Process additional art types in artist folder + AddLocalArtwork(addedart, MediaTypeArtist, artist.strArtist, artfolder); + + // Process remote artist art filling gaps with first of scraped art URLs + AddRemoteArtwork(addedart, MediaTypeArtist, artist.thumbURL); + + int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL); + + for (const auto& it : addedart) + { + // Cache thumb, fanart and other whitelisted artwork immediately + // (other art types will be cached when first displayed) + if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" || + it.first == "fanart") + CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second); + auto ret = artist.art.insert(it); + if (ret.second) + m_musicDatabase.SetArtForItem(artist.idArtist, MediaTypeArtist, it.first, it.second); + } + return addedart.size() > 0; +} + +bool CMusicInfoScanner::AddAlbumArtwork(CAlbum& album) +{ + // Fetch album path and any subfolders (disc sets). + // No paths found when songs from different albums are in one folder + std::vector<std::pair<std::string, int>> paths; + m_musicDatabase.GetAlbumPaths(album.idAlbum, paths); + for (const auto& pathpair : paths) + { + if (album.strPath.empty()) + album.strPath = pathpair.first.c_str(); + else + // When more than one album path is the common path + URIUtils::GetCommonPath(album.strPath, pathpair.first.c_str()); + } + + if (!album.thumbURL.HasUrls() && album.strPath.empty()) + return false; // No local or scraped possible art to process + + if (album.art.empty()) + m_musicDatabase.GetArtForItem(album.idAlbum, MediaTypeAlbum, album.art); + auto thumb = album.art.find("thumb"); // Find "thumb", may want to replace it + + bool replaceThumb = paths.size() > 1; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART)) + { + // When "prefer online album art" enabled and we have a thumb as embedded art + // then replace it if we find a scraped cover + if (thumb != album.art.end() && StringUtils::StartsWith(thumb->second, "image://")) + replaceThumb = true; + } + + std::map<std::string, std::string> addedart; + std::string strArt; + + // Fetch local art from album folder + // Handle thumbs separately, can be from multiple confgurable file names + if (replaceThumb || thumb == album.art.end()) + { + if (!album.strPath.empty()) + { // Local music thumbnail images named by "musiclibrary.musicthumbs" + CFileItem item(album.strPath, true); + strArt = item.GetUserMusicThumb(true); + } + if (strArt.empty()) + strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType("thumb")); + if (!strArt.empty()) + { + if (thumb != album.art.end()) + album.art.erase(thumb); + addedart.insert(std::make_pair("thumb", strArt)); + } + } + // Process additional art types in album folder + AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, album.strPath); + + // Fetch local art from disc subfolders + if (paths.size() > 1) + { + CMusicThumbLoader loader; + std::string firstDiscThumb; + int iDiscThumb = 10000; + for (const auto& pathpair : paths) + { + strArt.clear(); + + int discnum = m_musicDatabase.GetDiscnumberForPathID(pathpair.second); + if (discnum > 0) + { + // Handle thumbs separately. Get thumb for path from textures db cached during scan + // (could be embedded or local file from multiple confgurable file names) + CFileItem item(pathpair.first.c_str(), true); + std::string strArtType = StringUtils::Format("{}{}", "thumb", discnum); + strArt = loader.GetCachedImage(item, "thumb"); + if (strArt.empty()) + strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType(strArtType)); + if (!strArt.empty()) + { + addedart.insert(std::make_pair(strArtType, strArt)); + // Store thumb of first disc with a thumb + if (discnum < iDiscThumb) + { + iDiscThumb = discnum; + firstDiscThumb = strArt; + } + } + } + // Process additional art types in disc subfolder + AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, pathpair.first, discnum); + } + // Finally if we still don't have album thumb then use the art from the + // first disc in the set with a thumb + if (!firstDiscThumb.empty() && album.art.find("thumb") == album.art.end()) + { + m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, "thumb", firstDiscThumb); + // Assign art as folder thumb (in textures db) as well + + CFileItem albumItem(album.strPath, true); + loader.SetCachedImage(albumItem, "thumb", firstDiscThumb); + } + } + + // Process remote album art filling gaps with first of scraped art URLs + AddRemoteArtwork(addedart, MediaTypeAlbum, album.thumbURL); + + int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL); + for (const auto& it : addedart) + { + // Cache thumb, fanart and whitelisted artwork immediately + // (other art types will be cached when first displayed) + if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" || + it.first == "fanart") + CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second); + + auto ret = album.art.insert(it); + if (ret.second) + m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, it.first, it.second); + } + return addedart.size() > 0; +} + +std::vector<CVariant> CMusicInfoScanner::GetArtWhitelist(const MediaType& mediaType, int iArtLevel) +{ + std::vector<CVariant> whitelistarttypes; + if (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC) + { + // Basic artist artwork = thumb + fanart (but not "family" fanart1, fanart2 etc.) + // Basic album artwork = thumb only, thumb handled separately not in whitelist + if (mediaType == MediaTypeArtist) + whitelistarttypes.emplace_back("fanart"); + } + else + { + if (mediaType == MediaTypeArtist) + whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST); + else + whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST); + } + + return whitelistarttypes; +} + +bool CMusicInfoScanner::AddLocalArtwork(std::map<std::string, std::string>& art, + const std::string& mediaType, + const std::string& mediaName, + const std::string& artfolder, + int discnum) +{ + if (artfolder.empty()) + return false; + + int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL); + + std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel); + bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) || + ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) && + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART)); + + // Not useall and empty whitelist means no extra art is picked up from either place + if (!bUseAll && whitelistarttypes.empty()) + return false; + + // Image files used as thumbs + std::vector<CVariant> thumbs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS); + + // Find local art + CFileItemList availableArtFiles; + CDirectory::GetDirectory(artfolder, availableArtFiles, + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO); + + for (const auto& artFile : availableArtFiles) + { + if (artFile->m_bIsFolder) + continue; + std::string strCandidate = URIUtils::GetFileName(artFile->GetPath()); + // Strip media name + if (!mediaName.empty() && StringUtils::StartsWith(strCandidate, mediaName)) + strCandidate.erase(0, mediaName.length()); + StringUtils::ToLower(strCandidate); + // Skip files already used as "thumb" + // Typically folder.jpg but can be from multiple confgurable file names + if (std::find(thumbs.begin(), thumbs.end(), strCandidate) != thumbs.end()) + continue; + // Grab and strip file extension + std::string strExt; + size_t period = strCandidate.find_last_of("./\\"); + if (period != std::string::npos && strCandidate[period] == '.') + { + strExt = strCandidate.substr(period); // ".jpg", ".png" etc. + strCandidate.erase(period); // "abc16" for file Abc16.jpg + } + if (strCandidate.empty()) + continue; + // Validate art type name + size_t last_index = strCandidate.find_last_not_of("0123456789"); + std::string strDigits = strCandidate.substr(last_index + 1); + std::string strFamily = strCandidate.substr(0, last_index + 1); // "abc" of "abc16" + if (strFamily.empty()) + continue; + if (!MUSIC_UTILS::IsValidArtType(strCandidate)) + continue; + // Disc specific art from disc subfolder + // Skip art where digits of filename do not match disc number + if (discnum > 0 && !strDigits.empty() && (atoi(strDigits.c_str()) != discnum)) + continue; + + // Use all art, or check for basic level art in whitelist exactly allowing for disc number, + // or for custom art check whitelist contains art type family (strip trailing digits) + // e.g. 'fanart', 'fanart1', 'fanart2' etc. all match whitelist entry 'fanart' + std::string strCheck = strCandidate; + if (discnum > 0 || iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) + strCheck = strFamily; + if (bUseAll || std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strCheck) != + whitelistarttypes.end()) + { + if (!strDigits.empty()) + { + // Catch any variants of music thumbs e.g. folder2.jpg as "thumb2" + // Used for disc sets when files all in one album folder + if (std::find(thumbs.begin(), thumbs.end(), strFamily + strExt) != thumbs.end()) + strCandidate = "thumb" + strDigits; + } + else if (discnum > 0) + // Append disc number when candidate art type (and file) not have it + strCandidate += std::to_string(discnum); + + if (art.find(strCandidate) == art.end()) + art.insert(std::make_pair(strCandidate, artFile->GetPath())); + } + } + + return art.size() > 0; +} + +bool CMusicInfoScanner::AddRemoteArtwork(std::map<std::string, std::string>& art, + const std::string& mediaType, + const CScraperUrl& thumbURL) +{ + int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL); + + std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel); + bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) || + ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) && + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART)); + + // not useall and empty whitelist means no extra art is picked up from either place + if (!bUseAll && whitelistarttypes.empty()) + return false; + + // Add online art + // Done for artists and albums, so not need repeating at disc level + for (const auto& url : thumbURL.GetUrls()) + { + // Art type is encoded into the scraper XML held in thumbURL as optional "aspect=" field. + // Those URL without aspect are also returned for all other type values. + // Loop through all the first URLS of each type except "thumb" and add if art missing + if (url.m_aspect.empty() || url.m_aspect == "thumb") + continue; + if (!bUseAll) + { // Check whitelist for art type family e.g. "discart" for aspect="discart2" + std::string strName = url.m_aspect; + if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC) + StringUtils::TrimRight(strName, "0123456789"); + if (std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strName) == + whitelistarttypes.end()) + continue; + } + if (art.find(url.m_aspect) == art.end()) + { + std::string strArt = CScraperUrl::GetThumbUrl(url); + if (!strArt.empty()) + art.insert(std::make_pair(url.m_aspect, strArt)); + } + } + + return art.size() > 0; +} + +// This function is the Run() function of the IRunnable +// CFileCountReader and runs in a separate thread. +void CMusicInfoScanner::Run() +{ + int count = 0; + for (auto& it : m_pathsToScan) + { + count += CountFilesRecursively(it); + } + m_itemCount = count; +} + +// Recurse through all folders we scan and count files +int CMusicInfoScanner::CountFilesRecursively(const std::string& strPath) +{ + // load subfolder + CFileItemList items; + CDirectory::GetDirectory(strPath, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), DIR_FLAG_NO_FILE_DIRS); + + if (m_bStop) + return 0; + + // true for recursive counting + int count = CountFiles(items, true); + return count; +} + +int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive) +{ + int count = 0; + for (int i=0; i<items.Size(); ++i) + { + const CFileItemPtr pItem=items[i]; + + if (recursive && pItem->m_bIsFolder) + count+=CountFilesRecursively(pItem->GetPath()); + else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO()) + count++; + } + return count; +} diff --git a/xbmc/music/infoscanner/MusicInfoScanner.h b/xbmc/music/infoscanner/MusicInfoScanner.h new file mode 100644 index 0000000..d5d5470 --- /dev/null +++ b/xbmc/music/infoscanner/MusicInfoScanner.h @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "InfoScanner.h" +#include "MusicAlbumInfo.h" +#include "MusicInfoScraper.h" +#include "music/MusicDatabase.h" +#include "threads/IRunnable.h" +#include "threads/Thread.h" +#include "utils/ScraperUrl.h" + +class CAlbum; +class CArtist; +class CGUIDialogProgressBarHandle; + +namespace MUSIC_INFO +{ + +class CMusicInfoScanner : public IRunnable, public CInfoScanner +{ +public: + /*! \brief Flags for controlling the scanning process + */ + enum SCAN_FLAGS { SCAN_NORMAL = 0, + SCAN_ONLINE = 1 << 0, + SCAN_BACKGROUND = 1 << 1, + SCAN_RESCAN = 1 << 2, + SCAN_ARTISTS = 1 << 3, + SCAN_ALBUMS = 1 << 4 }; + + CMusicInfoScanner(); + ~CMusicInfoScanner() override; + + void Start(const std::string& strDirectory, int flags); + void FetchAlbumInfo(const std::string& strDirectory, bool refresh = false); + void FetchArtistInfo(const std::string& strDirectory, bool refresh = false); + void Stop(); + + /*! \brief Categorize FileItems into Albums, Songs, and Artists + This takes a list of FileItems and turns it into a tree of Albums, + Artists, and Songs. + Albums are defined uniquely by the album name and album artist. + + \param songs [in/out] list of songs to categorise - albumartist field may be altered. + \param albums [out] albums found within these songs. + */ + static void FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap = NULL); + + /*! \brief Scrape additional album information and update the music database with it. + Given an album, search for it using the given scraper. + If info is found, update the database and artwork with the new + information. + \param album [in/out] the album to update + \param scraper [in] the album scraper to use + \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET UpdateAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL); + + /*! \brief Scrape additional artist information and update the music database with it. + Given an artist, search for it using the given scraper. + If info is found, update the database and artwork with the new + information. + \param artist [in/out] the artist to update + \param scraper [in] the artist scraper to use + \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET UpdateArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL); + +protected: + virtual void Process(); + bool DoScan(const std::string& strDirectory) override; + + + /*! \brief Find art for albums + Based on the albums in the folder, finds whether we have unique album art + and assigns to the album if we do. + + In order of priority: + 1. If there is a single album in the folder, then the folder art is assigned to the album. + 2. We find the art for each song. A .tbn file takes priority over embedded art. + 3. If we have a unique piece of art for all songs in the album, we assign that to the album + and remove that art from each song so that they inherit from the album. + 4. If there is not a unique piece of art for each song, then no art is assigned + to the album. + + \param albums [in/out] list of albums to categorise - art field may be altered. + \param path [in] path containing albums. + */ + static void FindArtForAlbums(VECALBUMS &albums, const std::string &path); + + /*! \brief Scrape additional album information and update the database. + Search for the given album using the given scraper. + If info is found, update the database and artwork with the new + information. + \param album [in/out] the album to update + \param scraper [in] the album scraper to use + \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET UpdateDatabaseAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL); + + /*! \brief Scrape additional artist information and update the database. + Search for the given artist using the given scraper. + If info is found, update the database and artwork with the new + information. + \param artist [in/out] the artist to update + \param scraper [in] the artist scraper to use + \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET UpdateDatabaseArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL); + + /*! \brief Using the scrapers download metadata for an album + Given a CAlbum style struct containing some data about an album, query + the scrapers to try and get more information about the album. The responsibility + is with the caller to do something with that information. It will be passed back + in a MusicInfo struct, which you can save, display to the user or throw away. + \param album [in] a partially or fully filled out album structure containing the search query + \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path + \param albumInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper + \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the artist, or use artist name? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET DownloadAlbumInfo(const CAlbum& album, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicAlbumInfo& albumInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL); + + /*! \brief Using the scrapers download metadata for an artist + Given a CAlbum style struct containing some data about an artist, query + the scrapers to try and get more information about the artist. The responsibility + is with the caller to do something with that information. It will be passed back + in a MusicInfo struct, which you can save, display to the user or throw away. + \param artist [in] a partially or fully filled out artist structure containing the search query + \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path + \param artistInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper + \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the album, or use album and artist name? + \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required + */ + INFO_RET DownloadArtistInfo(const CArtist& artist, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicArtistInfo& artistInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL); + + /*! \brief Get the types of art for an artist or album that are to be + automatically fetched from local files during scanning + \param mediaType [in] artist or album + \param iArtLevel [in] art level + \return vector of art types that are to be fetched during scanning + */ + std::vector<CVariant> GetArtWhitelist(const MediaType& mediaType, int iArtLevel); + + /*! \brief Add extra local artwork for albums and artists + This common utility scans the given folder for local (non-thumb) art. + The art types checked are determined by whitelist and usealllocalart settings. + \param art [in/out] map of art type and file location (URL or path) pairs + \param mediaType [in] artist or album + \param mediaName [in] artist or album name that may be stripped from image file names + \param artfolder [in] path of folder containing local image files + \return true when art is added + */ + bool AddLocalArtwork(std::map<std::string, std::string>& art, + const std::string& mediaType, + const std::string& mediaName, + const std::string& artfolder, + int discnum = 0); + + /*! \brief Add extra remote artwork for albums and artists + This common utility fills the gaps in artwork using the first available art of each type from the + possible art URL results of previous scraping. + The art types applied are determined by whitelist and usealllocalart settings. + \param art [in/out] map of art type and file location (URL or path) pairs + \param mediaType [in] artist or album + \param thumbURL [in] URLs for potential remote artwork (provided by scrapers) + \return true when art is added + */ + bool AddRemoteArtwork(std::map<std::string, std::string>& art, + const std::string& mediaType, + const CScraperUrl& thumbURL); + + /*! \brief Add art for an artist + This scans the given folder for local art and/or applies the first available art of each type + from the possible art URLs previously scraped. Art is added to any already stored by previous + scanning etc.The art types processed are determined by whitelist and other art settings. + When usealllocalart is enabled then all local image files are applied as art (providing name is + valid for an art type), and then the URL list of remote art is checked adding the first available + image of each art type not yet in the art map. + Art found is saved in the album structure and the music database. The images found are cached. + \param artist [in/out] an artist, the art is set + \param artfolder [in] path of the location to search for local art files + \return true when art is added + */ + bool AddArtistArtwork(CArtist& artist, const std::string& artfolder); + + /*! \brief Add art for an album + This scans the album folder, and any disc set subfolders, for local art and/or applies the first + available art of each type from the possible art URLs previously scraped. Only those subfolders + beneath the album folder containing music files tagged with same unique disc number are scanned. + Art is added to any already stored by previous scanning, only "thumb" is optionally replaced. + The art types processed are determined by whitelist and other art settings. When usealllocalart is + enabled then all local image files are applied as art (providing name is valid for an art type), + and then the URL list of remote art is checked adding the first available image of each art type + not yet in the art map. + Art found is saved in the album structure and the music database. The images found are cached. + \param artist [in/out] an album, the art is set + \return true when art is added + */ + bool AddAlbumArtwork(CAlbum& album); + + /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems + Given a list of FileItems, scan in the tags for those FileItems + and populate a new FileItemList with the files that were successfully scanned. + Add album to library, populate a list of album ids added for possible scraping later. + Any files which couldn't be scanned (no/bad tags) are discarded in the process. + \param items [in] list of FileItems to scan + \param scannedItems [in] list to populate with the scannedItems + */ + int RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items); + + void RetrieveLocalArt(); + void ScrapeInfoAddedAlbums(); + + /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems + Given a list of FileItems, scan in the tags for those FileItems + and populate a new FileItemList with the files that were successfully scanned. + Any files which couldn't be scanned (no/bad tags) are discarded in the process. + \param items [in] list of FileItems to scan + \param scannedItems [in] list to populate with the scannedItems + */ + INFO_RET ScanTags(const CFileItemList& items, CFileItemList& scannedItems); + int GetPathHash(const CFileItemList &items, std::string &hash); + + void Run() override; + int CountFiles(const CFileItemList& items, bool recursive); + int CountFilesRecursively(const std::string& strPath); + + /*! \brief Resolve a MusicBrainzID to a URL + If we have a MusicBrainz ID for an artist or album, + resolve it to an MB URL and set up the scrapers accordingly. + + \param preferredScraper [in] A ScraperPtr to the preferred album/artist scraper. + \param musicBrainzURL [out] will be populated with the MB URL for the artist/album. + */ + bool ResolveMusicBrainz(const std::string &strMusicBrainzID, const ADDON::ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL); + + void ScannerWait(unsigned int milliseconds); + + int m_currentItem; + int m_itemCount; + bool m_bStop; + bool m_needsCleanup = false; + int m_scanType = 0; // 0 - load from files, 1 - albums, 2 - artists + int m_idSourcePath; + CMusicDatabase m_musicDatabase; + + std::set<int> m_albumsAdded; + + std::set<std::string> m_seenPaths; + int m_flags; + CThread m_fileCountReader; +}; +} diff --git a/xbmc/music/infoscanner/MusicInfoScraper.cpp b/xbmc/music/infoscanner/MusicInfoScraper.cpp new file mode 100644 index 0000000..630c2a7 --- /dev/null +++ b/xbmc/music/infoscanner/MusicInfoScraper.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoScraper.h" + +#include "filesystem/CurlFile.h" +#include "utils/log.h" + +using namespace MUSIC_GRABBER; +using namespace ADDON; +using namespace std::chrono_literals; + +CMusicInfoScraper::CMusicInfoScraper(const ADDON::ScraperPtr& scraper) + : CThread("MusicInfoScraper"), m_scraper(scraper) +{ + m_bSucceeded=false; + m_bCanceled=false; + m_iAlbum=-1; + m_iArtist=-1; + m_http = new XFILE::CCurlFile; +} + +CMusicInfoScraper::~CMusicInfoScraper(void) +{ + StopThread(); + delete m_http; +} + +int CMusicInfoScraper::GetAlbumCount() const +{ + return (int)m_vecAlbums.size(); +} + +int CMusicInfoScraper::GetArtistCount() const +{ + return (int)m_vecArtists.size(); +} + +CMusicAlbumInfo& CMusicInfoScraper::GetAlbum(int iAlbum) +{ + return m_vecAlbums[iAlbum]; +} + +CMusicArtistInfo& CMusicInfoScraper::GetArtist(int iArtist) +{ + return m_vecArtists[iArtist]; +} + +void CMusicInfoScraper::FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist /* = "" */) +{ + m_strAlbum=strAlbum; + m_strArtist=strArtist; + m_bSucceeded=false; + StopThread(); + Create(); +} + +void CMusicInfoScraper::FindArtistInfo(const std::string& strArtist) +{ + m_strArtist=strArtist; + m_bSucceeded=false; + StopThread(); + Create(); +} + +void CMusicInfoScraper::FindAlbumInfo() +{ + m_vecAlbums = m_scraper->FindAlbum(*m_http, m_strAlbum, m_strArtist); + m_bSucceeded = !m_vecAlbums.empty(); +} + +void CMusicInfoScraper::FindArtistInfo() +{ + m_vecArtists = m_scraper->FindArtist(*m_http, m_strArtist); + m_bSucceeded = !m_vecArtists.empty(); +} + +void CMusicInfoScraper::LoadAlbumInfo(int iAlbum) +{ + m_iAlbum=iAlbum; + m_iArtist=-1; + StopThread(); + Create(); +} + +void CMusicInfoScraper::LoadArtistInfo(int iArtist, const std::string &strSearch) +{ + m_iAlbum=-1; + m_iArtist=iArtist; + m_strSearch=strSearch; + StopThread(); + Create(); +} + +void CMusicInfoScraper::LoadAlbumInfo() +{ + if (m_iAlbum<0 || m_iAlbum>=(int)m_vecAlbums.size()) + return; + + CMusicAlbumInfo& album=m_vecAlbums[m_iAlbum]; + // Clear album artist credits + album.GetAlbum().artistCredits.clear(); + if (album.Load(*m_http,m_scraper)) + m_bSucceeded=true; +} + +void CMusicInfoScraper::LoadArtistInfo() +{ + if (m_iArtist<0 || m_iArtist>=(int)m_vecArtists.size()) + return; + + CMusicArtistInfo& artist=m_vecArtists[m_iArtist]; + artist.GetArtist().strArtist.clear(); + if (artist.Load(*m_http,m_scraper,m_strSearch)) + m_bSucceeded=true; +} + +bool CMusicInfoScraper::Completed() +{ + return Join(10ms); +} + +bool CMusicInfoScraper::Succeeded() +{ + return !m_bCanceled && m_bSucceeded; +} + +void CMusicInfoScraper::Cancel() +{ + m_http->Cancel(); + m_bCanceled=true; + m_http->Reset(); +} + +bool CMusicInfoScraper::IsCanceled() +{ + return m_bCanceled; +} + +void CMusicInfoScraper::OnStartup() +{ + m_bSucceeded=false; + m_bCanceled=false; +} + +void CMusicInfoScraper::Process() +{ + try + { + if (m_strAlbum.size()) + { + FindAlbumInfo(); + m_strAlbum.clear(); + m_strArtist.clear(); + } + else if (m_strArtist.size()) + { + FindArtistInfo(); + m_strArtist.clear(); + } + if (m_iAlbum>-1) + { + LoadAlbumInfo(); + m_iAlbum=-1; + } + if (m_iArtist>-1) + { + LoadArtistInfo(); + m_iArtist=-1; + } + } + catch(...) + { + CLog::Log(LOGERROR, "Exception in CMusicInfoScraper::Process()"); + } +} + +bool CMusicInfoScraper::CheckValidOrFallback(const std::string &fallbackScraper) +{ + return true; +//! @todo Handle fallback mechanism + /* + if (m_scraper->Path() != fallbackScraper && + parser.Load("special://xbmc/system/scrapers/music/" + fallbackScraper)) + { + CLog::Log(LOGWARNING, "{} - scraper {} fails to load, falling back to {}", __FUNCTION__, m_info.strPath, fallbackScraper); + m_info.strPath = fallbackScraper; + m_info.strContent = "albums"; + m_info.strTitle = parser.GetName(); + m_info.strDate = parser.GetDate(); + m_info.strFramework = parser.GetFramework(); + m_info.strLanguage = parser.GetLanguage(); + m_info.settings.LoadSettingsXML("special://xbmc/system/scrapers/music/" + m_info.strPath); + return true; + } + return false; */ +} diff --git a/xbmc/music/infoscanner/MusicInfoScraper.h b/xbmc/music/infoscanner/MusicInfoScraper.h new file mode 100644 index 0000000..fcab576 --- /dev/null +++ b/xbmc/music/infoscanner/MusicInfoScraper.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "MusicAlbumInfo.h" +#include "MusicArtistInfo.h" +#include "addons/Scraper.h" +#include "threads/Thread.h" + +#include <vector> + +namespace XFILE +{ +class CurlFile; +} + +namespace MUSIC_GRABBER +{ +class CMusicInfoScraper : public CThread +{ +public: + explicit CMusicInfoScraper(const ADDON::ScraperPtr &scraper); + ~CMusicInfoScraper(void) override; + void FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist = ""); + void LoadAlbumInfo(int iAlbum); + void FindArtistInfo(const std::string& strArtist); + void LoadArtistInfo(int iArtist, const std::string &strSearch); + bool Completed(); + bool Succeeded(); + void Cancel(); + bool IsCanceled(); + int GetAlbumCount() const; + int GetArtistCount() const; + CMusicAlbumInfo& GetAlbum(int iAlbum); + CMusicArtistInfo& GetArtist(int iArtist); + std::vector<CMusicArtistInfo>& GetArtists() + { + return m_vecArtists; + } + std::vector<CMusicAlbumInfo>& GetAlbums() + { + return m_vecAlbums; + } + void SetScraperInfo(const ADDON::ScraperPtr& scraper) + { + m_scraper = scraper; + } + /*! + \brief Checks whether we have a valid scraper. If not, we try the fallbackScraper + First tests the current scraper for validity by loading it. If it is not valid we + attempt to load the fallback scraper. If this is also invalid we return false. + \param fallbackScraper name of scraper to use as a fallback + \return true if we have a valid scraper (or the default is valid). + */ + bool CheckValidOrFallback(const std::string &fallbackScraper); +protected: + void FindAlbumInfo(); + void LoadAlbumInfo(); + void FindArtistInfo(); + void LoadArtistInfo(); + void OnStartup() override; + void Process() override; + std::vector<CMusicAlbumInfo> m_vecAlbums; + std::vector<CMusicArtistInfo> m_vecArtists; + std::string m_strAlbum; + std::string m_strArtist; + std::string m_strSearch; + int m_iAlbum; + int m_iArtist; + bool m_bSucceeded; + bool m_bCanceled; + XFILE::CCurlFile* m_http; + ADDON::ScraperPtr m_scraper; +}; + +} diff --git a/xbmc/music/jobs/CMakeLists.txt b/xbmc/music/jobs/CMakeLists.txt new file mode 100644 index 0000000..8ee0f5d --- /dev/null +++ b/xbmc/music/jobs/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES MusicLibraryJob.cpp + MusicLibraryProgressJob.cpp + MusicLibraryCleaningJob.cpp + MusicLibraryExportJob.cpp + MusicLibraryImportJob.cpp + MusicLibraryScanningJob.cpp) + +set(HEADERS MusicLibraryJob.h + MusicLibraryProgressJob.h + MusicLibraryCleaningJob.h + MusicLibraryExportJob.h + MusicLibraryImportJob.h + MusicLibraryScanningJob.h) + +core_add_library(music_jobs) diff --git a/xbmc/music/jobs/MusicLibraryCleaningJob.cpp b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp new file mode 100644 index 0000000..4361566 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryCleaningJob.h" + +#include "dialogs/GUIDialogProgress.h" +#include "music/MusicDatabase.h" + +CMusicLibraryCleaningJob::CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog) + : CMusicLibraryProgressJob(nullptr) +{ + if (progressDialog) + SetProgressIndicators(nullptr, progressDialog); + SetAutoClose(true); +} + +CMusicLibraryCleaningJob::~CMusicLibraryCleaningJob() = default; + +bool CMusicLibraryCleaningJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CMusicLibraryCleaningJob* cleaningJob = dynamic_cast<const CMusicLibraryCleaningJob*>(job); + if (cleaningJob == nullptr) + return false; + + return true; +} + +bool CMusicLibraryCleaningJob::Work(CMusicDatabase &db) +{ + db.Cleanup(GetProgressDialog()); + return true; +} diff --git a/xbmc/music/jobs/MusicLibraryCleaningJob.h b/xbmc/music/jobs/MusicLibraryCleaningJob.h new file mode 100644 index 0000000..6e893c6 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryCleaningJob.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "music/jobs/MusicLibraryProgressJob.h" + +#include <set> + +/*! + \brief Music library job implementation for cleaning the video library. +*/ +class CMusicLibraryCleaningJob : public CMusicLibraryProgressJob +{ +public: + /*! + \brief Creates a new music library cleaning job. + \param[in] progressDialog Progress dialog to be used to display the cleaning progress + */ + CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog); + ~CMusicLibraryCleaningJob() override; + + // specialization of CJob + const char *GetType() const override { return "MusicLibraryCleaningJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CMusicLibraryJob + bool Work(CMusicDatabase &db) override; + +private: + +}; diff --git a/xbmc/music/jobs/MusicLibraryExportJob.cpp b/xbmc/music/jobs/MusicLibraryExportJob.cpp new file mode 100644 index 0000000..ca63d13 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryExportJob.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryExportJob.h" + +#include "dialogs/GUIDialogProgress.h" +#include "music/MusicDatabase.h" +#include "settings/LibExportSettings.h" + +CMusicLibraryExportJob::CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog) + : CMusicLibraryProgressJob(NULL), + m_settings(settings) +{ + if (progressDialog) + SetProgressIndicators(NULL, progressDialog); + SetAutoClose(true); +} + +CMusicLibraryExportJob::~CMusicLibraryExportJob() = default; + +bool CMusicLibraryExportJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CMusicLibraryExportJob* exportJob = dynamic_cast<const CMusicLibraryExportJob*>(job); + if (exportJob == NULL) + return false; + + return !(m_settings != exportJob->m_settings); +} + +bool CMusicLibraryExportJob::Work(CMusicDatabase &db) +{ + db.ExportToXML(m_settings, GetProgressDialog()); + + return true; +} diff --git a/xbmc/music/jobs/MusicLibraryExportJob.h b/xbmc/music/jobs/MusicLibraryExportJob.h new file mode 100644 index 0000000..4dc759e --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryExportJob.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "MusicLibraryProgressJob.h" +#include "settings/LibExportSettings.h" + +class CGUIDialogProgress; + +/*! + \brief Music library job implementation for exporting the music library. +*/ +class CMusicLibraryExportJob : public CMusicLibraryProgressJob +{ +public: + /*! + \brief Creates a new music library export job for the given paths. + + \param[in] settings Library export settings + \param[in] progressDialog Progress dialog to be used to display the export progress + */ + CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog); + + ~CMusicLibraryExportJob() override; + + // specialization of CJob + const char *GetType() const override { return "MusicLibraryExportJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CMusicLibraryJob + bool Work(CMusicDatabase &db) override; + +private: + CLibExportSettings m_settings; +}; diff --git a/xbmc/music/jobs/MusicLibraryImportJob.cpp b/xbmc/music/jobs/MusicLibraryImportJob.cpp new file mode 100644 index 0000000..6fcae05 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryImportJob.cpp @@ -0,0 +1,42 @@ +/* +* Copyright (C) 2017-2018 Team Kodi +* This file is part of Kodi - https://kodi.tv +* +* SPDX-License-Identifier: GPL-2.0-or-later +* See LICENSES/README.md for more information. +*/ + +#include "MusicLibraryImportJob.h" + +#include "dialogs/GUIDialogProgress.h" +#include "music/MusicDatabase.h" + +CMusicLibraryImportJob::CMusicLibraryImportJob(const std::string& xmlFile, CGUIDialogProgress* progressDialog) + : CMusicLibraryProgressJob(nullptr) + , m_xmlFile(xmlFile) +{ + if (progressDialog) + SetProgressIndicators(nullptr, progressDialog); + SetAutoClose(true); +} + +CMusicLibraryImportJob::~CMusicLibraryImportJob() = default; + +bool CMusicLibraryImportJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CMusicLibraryImportJob* importJob = dynamic_cast<const CMusicLibraryImportJob*>(job); + if (importJob == nullptr) + return false; + + return !(m_xmlFile != importJob->m_xmlFile); +} + +bool CMusicLibraryImportJob::Work(CMusicDatabase &db) +{ + db.ImportFromXML(m_xmlFile, GetProgressDialog()); + + return true; +} diff --git a/xbmc/music/jobs/MusicLibraryImportJob.h b/xbmc/music/jobs/MusicLibraryImportJob.h new file mode 100644 index 0000000..bcca44f --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryImportJob.h @@ -0,0 +1,42 @@ +/* +* Copyright (C) 2017-2018 Team Kodi +* This file is part of Kodi - https://kodi.tv +* +* SPDX-License-Identifier: GPL-2.0-or-later +* See LICENSES/README.md for more information. +*/ + +#pragma once + +#include "MusicLibraryProgressJob.h" + +class CGUIDialogProgress; + +/*! +\brief Music library job implementation for importing data to the music library. +*/ +class CMusicLibraryImportJob : public CMusicLibraryProgressJob +{ +public: + /*! + \brief Creates a new music library import job for the given xml file. + + \param[in] xmlFile xml file to import + \param[in] progressDialog Progress dialog to be used to display the import progress + */ + CMusicLibraryImportJob(const std::string &xmlFile, CGUIDialogProgress* progressDialog); + + ~CMusicLibraryImportJob() override; + + // specialization of CJob + const char *GetType() const override { return "MusicLibraryImportJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CMusicLibraryJob + bool Work(CMusicDatabase &db) override; + +private: + std::string m_xmlFile; +}; + diff --git a/xbmc/music/jobs/MusicLibraryJob.cpp b/xbmc/music/jobs/MusicLibraryJob.cpp new file mode 100644 index 0000000..281d7f9 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryJob.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryJob.h" + +#include "music/MusicDatabase.h" + +CMusicLibraryJob::CMusicLibraryJob() = default; + +CMusicLibraryJob::~CMusicLibraryJob() = default; + +bool CMusicLibraryJob::DoWork() +{ + CMusicDatabase db; + if (!db.Open()) + return false; + + return Work(db); +} diff --git a/xbmc/music/jobs/MusicLibraryJob.h b/xbmc/music/jobs/MusicLibraryJob.h new file mode 100644 index 0000000..81f68b7 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryJob.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/Job.h" + +class CMusicDatabase; + +/*! + \brief Basic implementation/interface of a CJob which interacts with the + music database. + */ +class CMusicLibraryJob : public CJob +{ +public: + ~CMusicLibraryJob() override; + + /*! + \brief Whether the job can be cancelled or not. + */ + virtual bool CanBeCancelled() const { return false; } + + /*! + \brief Tries to cancel the running job. + \return True if the job was cancelled, false otherwise + */ + virtual bool Cancel() { return false; } + + // implementation of CJob + bool DoWork() override; + const char *GetType() const override { return "MusicLibraryJob"; } + bool operator==(const CJob* job) const override { return false; } + +protected: + CMusicLibraryJob(); + + /*! + \brief Worker method to be implemented by an actual implementation. + + \param[in] db Already open music database to be used for interaction + \return True if the process succeeded, false otherwise + */ + virtual bool Work(CMusicDatabase &db) = 0; +}; diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.cpp b/xbmc/music/jobs/MusicLibraryProgressJob.cpp new file mode 100644 index 0000000..d07e85b --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryProgressJob.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryProgressJob.h" + +CMusicLibraryProgressJob::CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar) + : CProgressJob(progressBar) +{ } + +CMusicLibraryProgressJob::~CMusicLibraryProgressJob() = default; + +bool CMusicLibraryProgressJob::DoWork() +{ + bool result = CMusicLibraryJob::DoWork(); + + MarkFinished(); + + return result; +} diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.h b/xbmc/music/jobs/MusicLibraryProgressJob.h new file mode 100644 index 0000000..ae843da --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryProgressJob.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "music/jobs/MusicLibraryJob.h" +#include "utils/ProgressJob.h" + +/*! + \brief Combined base implementation of a music library job with a progress bar. + */ +class CMusicLibraryProgressJob : public CProgressJob, public CMusicLibraryJob +{ +public: + ~CMusicLibraryProgressJob() override; + + // implementation of CJob + bool DoWork() override; + const char *GetType() const override { return "CMusicLibraryProgressJob"; } + bool operator==(const CJob* job) const override { return false; } + +protected: + explicit CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar); +}; diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.cpp b/xbmc/music/jobs/MusicLibraryScanningJob.cpp new file mode 100644 index 0000000..d2626a1 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryScanningJob.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicLibraryScanningJob.h" + +#include "music/MusicDatabase.h" + +CMusicLibraryScanningJob::CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress /* = true */) + : m_scanner(), + m_directory(directory), + m_showProgress(showProgress), + m_flags(flags) +{ } + +CMusicLibraryScanningJob::~CMusicLibraryScanningJob() = default; + +bool CMusicLibraryScanningJob::Cancel() +{ + if (!m_scanner.IsScanning()) + return true; + + m_scanner.Stop(); + return true; +} + +bool CMusicLibraryScanningJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CMusicLibraryScanningJob* scanningJob = dynamic_cast<const CMusicLibraryScanningJob*>(job); + if (scanningJob == nullptr) + return false; + + return m_directory == scanningJob->m_directory && + m_flags == scanningJob->m_flags; +} + +bool CMusicLibraryScanningJob::Work(CMusicDatabase &db) +{ + m_scanner.ShowDialog(m_showProgress); + if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS) + // Scrape additional album information + m_scanner.FetchAlbumInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN); + else if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS) + // Scrape additional artist information + m_scanner.FetchArtistInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN); + else + // Scan tags from music files, and optionally scrape artist and album info + m_scanner.Start(m_directory, m_flags); + + return true; +} diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.h b/xbmc/music/jobs/MusicLibraryScanningJob.h new file mode 100644 index 0000000..6c5ea13 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryScanningJob.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/jobs/MusicLibraryJob.h" + +#include <string> + +/*! + \brief Music library job implementation for scanning items. + Uses CMusicInfoScanner for scanning and can be run with or + without a visible progress bar. + */ +class CMusicLibraryScanningJob : public CMusicLibraryJob +{ +public: + /*! + \brief Creates a new music library tag scanning and data scraping job. + \param[in] directory Directory to be scanned for new items + \param[in] flags What kind of scan to do + \param[in] showProgress Whether to show a progress bar or not + */ + CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress = true); + ~CMusicLibraryScanningJob() override; + + // specialization of CMusicLibraryJob + bool CanBeCancelled() const override { return true; } + bool Cancel() override; + + // specialization of CJob + const char *GetType() const override { return "MusicLibraryScanningJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CMusicLibraryJob + bool Work(CMusicDatabase &db) override; + +private: + MUSIC_INFO::CMusicInfoScanner m_scanner; + std::string m_directory; + bool m_showProgress; + int m_flags; +}; diff --git a/xbmc/music/tags/CMakeLists.txt b/xbmc/music/tags/CMakeLists.txt new file mode 100644 index 0000000..bbd3462 --- /dev/null +++ b/xbmc/music/tags/CMakeLists.txt @@ -0,0 +1,22 @@ +set(SOURCES MusicInfoTag.cpp + MusicInfoTagLoaderCDDA.cpp + MusicInfoTagLoaderDatabase.cpp + MusicInfoTagLoaderFactory.cpp + MusicInfoTagLoaderFFmpeg.cpp + MusicInfoTagLoaderShn.cpp + ReplayGain.cpp + TagLibVFSStream.cpp + TagLoaderTagLib.cpp) + +set(HEADERS ImusicInfoTagLoader.h + MusicInfoTag.h + MusicInfoTagLoaderCDDA.h + MusicInfoTagLoaderDatabase.h + MusicInfoTagLoaderFactory.h + MusicInfoTagLoaderFFmpeg.h + MusicInfoTagLoaderShn.h + ReplayGain.h + TagLibVFSStream.h + TagLoaderTagLib.h) + +core_add_library(music_tags) diff --git a/xbmc/music/tags/ImusicInfoTagLoader.h b/xbmc/music/tags/ImusicInfoTagLoader.h new file mode 100644 index 0000000..c3e07fe --- /dev/null +++ b/xbmc/music/tags/ImusicInfoTagLoader.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <string> + +class EmbeddedArt; + +namespace MUSIC_INFO +{ + class CMusicInfoTag; + class IMusicInfoTagLoader + { + public: + IMusicInfoTagLoader(void) = default; + virtual ~IMusicInfoTagLoader() = default; + + virtual bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) = 0; + }; +} diff --git a/xbmc/music/tags/MusicInfoTag.cpp b/xbmc/music/tags/MusicInfoTag.cpp new file mode 100644 index 0000000..62548bf --- /dev/null +++ b/xbmc/music/tags/MusicInfoTag.cpp @@ -0,0 +1,1303 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTag.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "music/Artist.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Archive.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <algorithm> + +using namespace MUSIC_INFO; + +CMusicInfoTag::CMusicInfoTag(void) +{ + Clear(); +} + +bool CMusicInfoTag::operator !=(const CMusicInfoTag& tag) const +{ + if (this == &tag) return false; + if (m_strURL != tag.m_strURL) return true; + if (m_strTitle != tag.m_strTitle) return true; + if (m_bCompilation != tag.m_bCompilation) return true; + if (m_artist != tag.m_artist) return true; + if (m_albumArtist != tag.m_albumArtist) return true; + if (m_strAlbum != tag.m_strAlbum) return true; + if (m_iDuration != tag.m_iDuration) return true; + if (m_strDiscSubtitle != tag.m_strDiscSubtitle) + return true; + if (m_iTrack != tag.m_iTrack) + return true; + if (m_albumReleaseType != tag.m_albumReleaseType) return true; + return false; +} + +int CMusicInfoTag::GetTrackNumber() const +{ + return (m_iTrack & 0xffff); +} + +int CMusicInfoTag::GetDiscNumber() const +{ + return (m_iTrack >> 16); +} + +int CMusicInfoTag::GetTrackAndDiscNumber() const +{ + return m_iTrack; +} + +int CMusicInfoTag::GetDuration() const +{ + return m_iDuration; +} + +const std::string& CMusicInfoTag::GetTitle() const +{ + return m_strTitle; +} + +const std::string& CMusicInfoTag::GetURL() const +{ + return m_strURL; +} + +const std::vector<std::string>& CMusicInfoTag::GetArtist() const +{ + return m_artist; +} + +const std::string CMusicInfoTag::GetArtistString() const +{ + if (!m_strArtistDesc.empty()) + return m_strArtistDesc; + else if (!m_artist.empty()) + return StringUtils::Join(m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + else + return StringUtils::Empty; +} + +const std::string& CMusicInfoTag::GetArtistSort() const +{ + return m_strArtistSort; +} + +const std::string& CMusicInfoTag::GetComposerSort() const +{ + return m_strComposerSort; +} + +const std::string& CMusicInfoTag::GetAlbum() const +{ + return m_strAlbum; +} + +const std::string& CMusicInfoTag::GetDiscSubtitle() const +{ + return m_strDiscSubtitle; +} + +const std::string& CMusicInfoTag::GetOriginalDate() const +{ + return m_strOriginalDate; +} + +const std::string MUSIC_INFO::CMusicInfoTag::GetOriginalYear() const +{ + return StringUtils::Left(m_strOriginalDate, 4); +} + +int CMusicInfoTag::GetAlbumId() const +{ + return m_iAlbumId; +} + +const std::vector<std::string>& CMusicInfoTag::GetAlbumArtist() const +{ + return m_albumArtist; +} + +const std::string CMusicInfoTag::GetAlbumArtistString() const +{ + if (!m_strAlbumArtistDesc.empty()) + return m_strAlbumArtistDesc; + if (!m_albumArtist.empty()) + return StringUtils::Join(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + else + return StringUtils::Empty; +} + +const std::string& CMusicInfoTag::GetAlbumArtistSort() const +{ + return m_strAlbumArtistSort; +} + +const std::vector<std::string>& CMusicInfoTag::GetGenre() const +{ + return m_genre; +} + +int CMusicInfoTag::GetDatabaseId() const +{ + return m_iDbId; +} + +const std::string &CMusicInfoTag::GetType() const +{ + return m_type; +} + +int CMusicInfoTag::GetYear() const +{ + return atoi(GetYearString().c_str()); +} + +std::string CMusicInfoTag::GetYearString() const +{ + /* Get year as YYYY from release or original dates depending on setting + This is how GUI and by year sorting swiches to using original year. + For ripper and non-library items (library entries have both values): + when release date missing try to fallback to original date + when original date missing use release date + */ + std::string value; + value = GetReleaseYear(); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE) || + value.empty()) + { + std::string origvalue = GetOriginalYear(); + if (!origvalue.empty()) + return origvalue; + } + return value; +} + +const std::string &CMusicInfoTag::GetComment() const +{ + return m_strComment; +} + +const std::string &CMusicInfoTag::GetMood() const +{ + return m_strMood; +} + +const std::string &CMusicInfoTag::GetRecordLabel() const +{ + return m_strRecordLabel; +} + +const std::string &CMusicInfoTag::GetLyrics() const +{ + return m_strLyrics; +} + +const std::string &CMusicInfoTag::GetCueSheet() const +{ + return m_cuesheet; +} + +float CMusicInfoTag::GetRating() const +{ + return m_Rating; +} + +int CMusicInfoTag::GetUserrating() const +{ + return m_Userrating; +} + +int CMusicInfoTag::GetVotes() const +{ + return m_Votes; +} + +int CMusicInfoTag::GetListeners() const +{ + return m_listeners; +} + +int CMusicInfoTag::GetPlayCount() const +{ + return m_iTimesPlayed; +} + +const CDateTime &CMusicInfoTag::GetLastPlayed() const +{ + return m_lastPlayed; +} + +const CDateTime &CMusicInfoTag::GetDateAdded() const +{ + return m_dateAdded; +} + +bool CMusicInfoTag::GetCompilation() const +{ + return m_bCompilation; +} + +bool CMusicInfoTag::GetBoxset() const +{ + return m_bBoxset; +} + +int CMusicInfoTag::GetTotalDiscs() const +{ + return m_iDiscTotal; +} + +const EmbeddedArtInfo &CMusicInfoTag::GetCoverArtInfo() const +{ + return m_coverArt; +} + +const ReplayGain& CMusicInfoTag::GetReplayGain() const +{ + return m_replayGain; +} + +CAlbum::ReleaseType CMusicInfoTag::GetAlbumReleaseType() const +{ + return m_albumReleaseType; +} + +int CMusicInfoTag::GetBPM() const +{ + return m_iBPM; +} + +int CMusicInfoTag::GetBitRate() const +{ + return m_bitrate; +} + +int CMusicInfoTag::GetSampleRate() const +{ + return m_samplerate; +} + +int CMusicInfoTag::GetNoOfChannels() const +{ + return m_channels; +} + +const std::string& CMusicInfoTag::GetReleaseDate() const +{ + return m_strReleaseDate; +} + +const std::string MUSIC_INFO::CMusicInfoTag::GetReleaseYear() const +{ + return StringUtils::Left(m_strReleaseDate, 4); +} + +// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status + +const std::string& CMusicInfoTag::GetAlbumReleaseStatus() const +{ + return m_strReleaseStatus; +} + +const std::string& CMusicInfoTag::GetStationName() const +{ + return m_stationName; +} + +const std::string& CMusicInfoTag::GetStationArt() const +{ + return m_stationArt; +} + +void CMusicInfoTag::SetURL(const std::string& strURL) +{ + m_strURL = strURL; +} + +void CMusicInfoTag::SetTitle(const std::string& strTitle) +{ + m_strTitle = Trim(strTitle); +} + +void CMusicInfoTag::SetArtist(const std::string& strArtist) +{ + if (!strArtist.empty()) + { + SetArtistDesc(strArtist); + SetArtist(StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + } + else + { + m_strArtistDesc.clear(); + m_artist.clear(); + } +} + +void CMusicInfoTag::SetArtist(const std::vector<std::string>& artists, bool FillDesc /* = false*/) +{ + m_artist = artists; + if (m_strArtistDesc.empty() || FillDesc) + { + SetArtistDesc(StringUtils::Join(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + } +} + +void CMusicInfoTag::SetArtistDesc(const std::string& strArtistDesc) +{ + m_strArtistDesc = strArtistDesc; +} + +void CMusicInfoTag::SetArtistSort(const std::string& strArtistsort) +{ + m_strArtistSort = strArtistsort; +} + +void CMusicInfoTag::SetComposerSort(const std::string& strComposerSort) +{ + m_strComposerSort = strComposerSort; +} + +void CMusicInfoTag::SetAlbum(const std::string& strAlbum) +{ + m_strAlbum = Trim(strAlbum); +} + +void CMusicInfoTag::SetAlbumId(const int iAlbumId) +{ + m_iAlbumId = iAlbumId; +} + +void CMusicInfoTag::SetAlbumArtist(const std::string& strAlbumArtist) +{ + if (!strAlbumArtist.empty()) + { + SetAlbumArtistDesc(strAlbumArtist); + SetAlbumArtist(StringUtils::Split(strAlbumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + } + else + { + m_strAlbumArtistDesc.clear(); + m_albumArtist.clear(); + } +} + +void CMusicInfoTag::SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc /* = false*/) +{ + m_albumArtist = albumArtists; + if (m_strAlbumArtistDesc.empty() || FillDesc) + SetAlbumArtistDesc(StringUtils::Join(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CMusicInfoTag::SetAlbumArtistDesc(const std::string& strAlbumArtistDesc) +{ + m_strAlbumArtistDesc = strAlbumArtistDesc; +} + +void CMusicInfoTag::SetAlbumArtistSort(const std::string& strAlbumArtistSort) +{ + m_strAlbumArtistSort = strAlbumArtistSort; +} + +void CMusicInfoTag::SetGenre(const std::string& strGenre, bool bTrim /* = false*/) +{ + if (!strGenre.empty()) + SetGenre(StringUtils::Split(strGenre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator), bTrim); + else + m_genre.clear(); +} + +void CMusicInfoTag::SetGenre(const std::vector<std::string>& genres, bool bTrim /* = false*/) +{ + m_genre = genres; + if (bTrim) + for (auto genre : m_genre) + StringUtils::Trim(genre); +} + +void CMusicInfoTag::SetYear(int year) +{ + // Parse integer year value into YYYY ISO8601 format (partial) date string + // Add century for to 2 digit numbers, 41 -> 1941, 40 -> 2040 + if (year > 99) + SetReleaseDate(StringUtils::Format("{:04}", year)); + else if (year > 40) + SetReleaseDate(StringUtils::Format("{:04}", 19 + year)); + else if (year > 0) + SetReleaseDate(StringUtils::Format("{:04}", 20 + year)); + else + m_strReleaseDate.clear(); +} + +void CMusicInfoTag::SetDatabaseId(int id, const std::string &type) +{ + m_iDbId = id; + m_type = type; +} + +void CMusicInfoTag::SetTrackNumber(int iTrack) +{ + m_iTrack = (m_iTrack & 0xffff0000) | (iTrack & 0xffff); +} + +void CMusicInfoTag::SetDiscNumber(int iDiscNumber) +{ + m_iTrack = (m_iTrack & 0xffff) | (iDiscNumber << 16); +} + +void CMusicInfoTag::SetDiscSubtitle(const std::string& strDiscSubtitle) +{ + m_strDiscSubtitle = strDiscSubtitle; +} + +void CMusicInfoTag::SetTotalDiscs(int iDiscTotal) +{ + m_iDiscTotal = iDiscTotal; +} + +void CMusicInfoTag::SetReleaseDate(const std::string& strReleaseDate) +{ + // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD + m_strReleaseDate = strReleaseDate; +} + +void CMusicInfoTag::SetOriginalDate(const std::string& strOriginalDate) +{ + // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD + m_strOriginalDate = strOriginalDate; +} + +void CMusicInfoTag::AddOriginalDate(const std::string& strDateYear) +{ + // Avoid overwriting YYYY-MM or YYYY-MM-DD (from DATE tag) with just YYYY (from YEAR tag) + if (strDateYear.size() > m_strOriginalDate.size()) + m_strOriginalDate = strDateYear; +} + +void CMusicInfoTag::AddReleaseDate(const std::string& strDateYear, bool isMonth /*= false*/) +{ + // Given MMDD (from ID3 v2.3 TDAT tag) set MM-DD part of ISO8601 string + if (isMonth && !strDateYear.empty()) + { + std::string strYYYY = GetReleaseYear(); + if (strYYYY.empty()) + strYYYY = "0000"; // Fake year when TYER not read yet + m_strReleaseDate = StringUtils::Format("{}-{}-{}", strYYYY, StringUtils::Left(strDateYear, 2), + StringUtils::Right(strDateYear, 2)); + } + // Given YYYY only (from YEAR tag) and already have YYYY-MM or YYYY-MM-DD (from DATE tag) + else if (strDateYear.size() == 4 && (m_strReleaseDate.size() > 4)) + { + // Have 0000-MM-DD where ID3 v2.3 TDAT tag read first, fill YYYY part from TYER + if (GetReleaseYear() == "0000") + StringUtils::Replace(m_strReleaseDate, "0000", strDateYear); + } + else + m_strReleaseDate = strDateYear; // Could be YYYY, YYYY-MM or YYYY-MM-DD +} + +void CMusicInfoTag::SetTrackAndDiscNumber(int iTrackAndDisc) +{ + m_iTrack = iTrackAndDisc; +} + +void CMusicInfoTag::SetDuration(int iSec) +{ + m_iDuration = iSec; +} + +void CMusicInfoTag::SetBitRate(int bitrate) +{ + m_bitrate = bitrate; +} + +void CMusicInfoTag::SetNoOfChannels(int channels) +{ + m_channels = channels; +} + +void CMusicInfoTag::SetSampleRate(int samplerate) +{ + m_samplerate = samplerate; +} + +void CMusicInfoTag::SetComment(const std::string& comment) +{ + m_strComment = comment; +} + +void CMusicInfoTag::SetMood(const std::string& mood) +{ + m_strMood = mood; +} + +void CMusicInfoTag::SetRecordLabel(const std::string& publisher) +{ + m_strRecordLabel = publisher; +} + +void CMusicInfoTag::SetCueSheet(const std::string& cueSheet) +{ + m_cuesheet = cueSheet; +} + +void CMusicInfoTag::SetLyrics(const std::string& lyrics) +{ + m_strLyrics = lyrics; +} + +void CMusicInfoTag::SetRating(float rating) +{ + //This value needs to be between 0-10 - 0 will unset the rating + rating = std::max(rating, 0.f); + rating = std::min(rating, 10.f); + + m_Rating = rating; +} + +void CMusicInfoTag::SetVotes(int votes) +{ + m_Votes = votes; +} + +void CMusicInfoTag::SetUserrating(int rating) +{ + //This value needs to be between 0-10 - 0 will unset the userrating + rating = std::max(rating, 0); + rating = std::min(rating, 10); + + m_Userrating = rating; +} + +void CMusicInfoTag::SetListeners(int listeners) +{ + m_listeners = std::max(listeners, 0); +} + +void CMusicInfoTag::SetPlayCount(int playcount) +{ + m_iTimesPlayed = playcount; +} + +void CMusicInfoTag::SetLastPlayed(const std::string& lastplayed) +{ + m_lastPlayed.SetFromDBDateTime(lastplayed); +} + +void CMusicInfoTag::SetLastPlayed(const CDateTime& lastplayed) +{ + m_lastPlayed = lastplayed; +} + +void CMusicInfoTag::SetDateAdded(const std::string& strDateAdded) +{ + m_dateAdded.SetFromDBDateTime(strDateAdded); +} + +void CMusicInfoTag::SetDateAdded(const CDateTime& dateAdded) +{ + m_dateAdded = dateAdded; +} + +void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const std::string& strDateUpdated) +{ + m_dateUpdated.SetFromDBDateTime(strDateUpdated); +} + +void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const CDateTime& dateUpdated) +{ + m_dateUpdated = dateUpdated; +} + +void MUSIC_INFO::CMusicInfoTag::SetDateNew(const std::string& strDateNew) +{ + m_dateNew.SetFromDBDateTime(strDateNew); +} + +void MUSIC_INFO::CMusicInfoTag::SetDateNew(const CDateTime& dateNew) +{ + m_dateNew = dateNew; +} + +void CMusicInfoTag::SetCompilation(bool compilation) +{ + m_bCompilation = compilation; +} + +void CMusicInfoTag::SetBoxset(bool boxset) +{ + m_bBoxset = boxset; +} + +void CMusicInfoTag::SetLoaded(bool bOnOff) +{ + m_bLoaded = bOnOff; +} + +bool CMusicInfoTag::Loaded() const +{ + return m_bLoaded; +} + +void CMusicInfoTag::SetBPM(int bpm) +{ + m_iBPM = bpm; +} + +void CMusicInfoTag::SetStationName(const std::string& strStationName) +{ + m_stationName = strStationName; +} + +const std::string& CMusicInfoTag::GetMusicBrainzTrackID() const +{ + return m_strMusicBrainzTrackID; +} + +const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistID() const +{ + return m_musicBrainzArtistID; +} + +const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistHints() const +{ + return m_musicBrainzArtistHints; +} + +const std::string& CMusicInfoTag::GetMusicBrainzAlbumID() const +{ + return m_strMusicBrainzAlbumID; +} + +const std::string & MUSIC_INFO::CMusicInfoTag::GetMusicBrainzReleaseGroupID() const +{ + return m_strMusicBrainzReleaseGroupID; +} + +const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistID() const +{ + return m_musicBrainzAlbumArtistID; +} + +const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistHints() const +{ + return m_musicBrainzAlbumArtistHints; +} + +const std::string &CMusicInfoTag::GetMusicBrainzReleaseType() const +{ + return m_strMusicBrainzReleaseType; +} + +void CMusicInfoTag::SetMusicBrainzTrackID(const std::string& strTrackID) +{ + m_strMusicBrainzTrackID = strTrackID; +} + +void CMusicInfoTag::SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId) +{ + m_musicBrainzArtistID = musicBrainzArtistId; +} + +void CMusicInfoTag::SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints) +{ + m_musicBrainzArtistHints = musicBrainzArtistHints; +} + +void CMusicInfoTag::SetMusicBrainzAlbumID(const std::string& strAlbumID) +{ + m_strMusicBrainzAlbumID = strAlbumID; +} + +void CMusicInfoTag::SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId) +{ + m_musicBrainzAlbumArtistID = musicBrainzAlbumArtistId; +} + +void CMusicInfoTag::SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints) +{ + m_musicBrainzAlbumArtistHints = musicBrainzAlbumArtistHints; +} + +void MUSIC_INFO::CMusicInfoTag::SetMusicBrainzReleaseGroupID(const std::string & strReleaseGroupID) +{ + m_strMusicBrainzReleaseGroupID = strReleaseGroupID; +} + +void CMusicInfoTag::SetMusicBrainzReleaseType(const std::string& ReleaseType) +{ + m_strMusicBrainzReleaseType = ReleaseType; +} + +void CMusicInfoTag::SetCoverArtInfo(size_t size, const std::string &mimeType) +{ + m_coverArt.Set(size, mimeType); +} + +void CMusicInfoTag::SetReplayGain(const ReplayGain& aGain) +{ + m_replayGain = aGain; +} + +void CMusicInfoTag::SetAlbumReleaseType(CAlbum::ReleaseType releaseType) +{ + m_albumReleaseType = releaseType; +} + +void CMusicInfoTag::SetType(const MediaType& mediaType) +{ + m_type = mediaType; +} + +// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status + +void CMusicInfoTag::SetAlbumReleaseStatus(const std::string& ReleaseStatus) +{ + m_strReleaseStatus = ReleaseStatus; +} + +void CMusicInfoTag::SetStationArt(const std::string& strStationArt) +{ + m_stationArt = strStationArt; +} + +void CMusicInfoTag::SetArtist(const CArtist& artist) +{ + SetArtist(artist.strArtist); + SetArtistSort(artist.strSortName); + SetAlbumArtist(artist.strArtist); + SetAlbumArtistSort(artist.strSortName); + SetMusicBrainzArtistID({ artist.strMusicBrainzArtistID }); + SetMusicBrainzAlbumArtistID({ artist.strMusicBrainzArtistID }); + SetGenre(artist.genre); + SetMood(StringUtils::Join(artist.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + SetDateAdded(artist.dateAdded); + SetDateUpdated(artist.dateUpdated); + SetDateNew(artist.dateNew); + SetDatabaseId(artist.idArtist, MediaTypeArtist); + + SetLoaded(); +} + +void CMusicInfoTag::SetAlbum(const CAlbum& album) +{ + Clear(); + //Set all artist information from album artist credits and artist description + SetArtistDesc(album.GetAlbumArtistString()); + SetArtist(album.GetAlbumArtist()); + SetArtistSort(album.GetAlbumArtistSort()); + SetMusicBrainzArtistID(album.GetMusicBrainzAlbumArtistID()); + SetAlbumArtistDesc(album.GetAlbumArtistString()); + SetAlbumArtist(album.GetAlbumArtist()); + SetAlbumArtistSort(album.GetAlbumArtistSort()); + SetMusicBrainzAlbumArtistID(album.GetMusicBrainzAlbumArtistID()); + SetAlbumId(album.idAlbum); + SetAlbum(album.strAlbum); + SetTitle(album.strAlbum); + SetMusicBrainzAlbumID(album.strMusicBrainzAlbumID); + SetMusicBrainzReleaseGroupID(album.strReleaseGroupMBID); + SetMusicBrainzReleaseType(album.strType); + SetAlbumReleaseStatus(album.strReleaseStatus); + SetGenre(album.genre); + SetMood(StringUtils::Join(album.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + SetRecordLabel(album.strLabel); + SetRating(album.fRating); + SetUserrating(album.iUserrating); + SetVotes(album.iVotes); + SetCompilation(album.bCompilation); + SetOriginalDate(album.strOrigReleaseDate); + SetReleaseDate(album.strReleaseDate); + SetBoxset(album.bBoxedSet); + SetAlbumReleaseType(album.releaseType); + SetDateAdded(album.dateAdded); + SetDateUpdated(album.dateUpdated); + SetDateNew(album.dateNew); + SetPlayCount(album.iTimesPlayed); + SetDatabaseId(album.idAlbum, MediaTypeAlbum); + SetLastPlayed(album.lastPlayed); + SetTotalDiscs(album.iTotalDiscs); + SetDuration(album.iAlbumDuration); + + SetLoaded(); +} + +void CMusicInfoTag::SetSong(const CSong& song) +{ + Clear(); + SetTitle(song.strTitle); + SetGenre(song.genre); + /* Set all artist information from song artist credits and artist description. + During processing e.g. Cue Sheets, song may only have artist description string + rather than a fully populated artist credits vector. + */ + if (!song.HasArtistCredits()) + SetArtist(song.GetArtistString()); //Sets both artist description string and artist vector from string + else + { + SetArtistDesc(song.GetArtistString()); + SetArtist(song.GetArtist()); + SetMusicBrainzArtistID(song.GetMusicBrainzArtistID()); + } + SetArtistSort(song.GetArtistSort()); + SetAlbum(song.strAlbum); + SetAlbumArtist(song.GetAlbumArtist()); //Only have album artist in song as vector, no desc or MBID + SetAlbumArtistSort(song.GetAlbumArtistSort()); + SetMusicBrainzTrackID(song.strMusicBrainzTrackID); + SetContributors(song.GetContributors()); + SetComment(song.strComment); + SetCueSheet(song.strCueSheet); + SetPlayCount(song.iTimesPlayed); + SetLastPlayed(song.lastPlayed); + SetDateAdded(song.dateAdded); + SetDateUpdated(song.dateUpdated); + SetDateNew(song.dateNew); + SetCoverArtInfo(song.embeddedArt.m_size, song.embeddedArt.m_mime); + SetRating(song.rating); + SetUserrating(song.userrating); + SetVotes(song.votes); + SetURL(song.strFileName); + SetReleaseDate(song.strReleaseDate); + SetOriginalDate(song.strOrigReleaseDate); + SetTrackAndDiscNumber(song.iTrack); + SetDiscSubtitle(song.strDiscSubtitle); + SetDuration(song.iDuration); + SetMood(song.strMood); + SetCompilation(song.bCompilation); + SetAlbumId(song.idAlbum); + SetDatabaseId(song.idSong, MediaTypeSong); + SetBPM(song.iBPM); + SetBitRate(song.iBitRate); + SetSampleRate(song.iSampleRate); + SetNoOfChannels(song.iChannels); + + if (song.replayGain.Get(ReplayGain::TRACK).Valid()) + m_replayGain.Set(ReplayGain::TRACK, song.replayGain.Get(ReplayGain::TRACK)); + if (song.replayGain.Get(ReplayGain::ALBUM).Valid()) + m_replayGain.Set(ReplayGain::ALBUM, song.replayGain.Get(ReplayGain::ALBUM)); + + SetLoaded(); +} + +void CMusicInfoTag::Serialize(CVariant& value) const +{ + value["url"] = m_strURL; + value["title"] = m_strTitle; + if (m_type.compare(MediaTypeArtist) == 0 && m_artist.size() == 1) + value["artist"] = m_artist[0]; + else + value["artist"] = m_artist; + // There are situations where the individual artist(s) are not queried from the song_artist and artist tables e.g. playlist, + // only artist description from song table. Since processing of the ARTISTS tag was added the individual artists may not always + // be accurately derived by simply splitting the artist desc. Hence m_artist is only populated when the individual artists are + // queried, whereas GetArtistString() will always return the artist description. + // To avoid empty artist array in JSON, when m_artist is empty then an attempt is made to split the artist desc into artists. + // A longer term solution would be to ensure that when individual artists are to be returned then the song_artist and artist tables + // are queried. + if (m_artist.empty()) + value["artist"] = StringUtils::Split(GetArtistString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + + value["displayartist"] = GetArtistString(); + value["displayalbumartist"] = GetAlbumArtistString(); + value["sortartist"] = GetArtistSort(); + value["album"] = m_strAlbum; + value["albumartist"] = m_albumArtist; + value["sortalbumartist"] = m_strAlbumArtistSort; + value["genre"] = m_genre; + value["duration"] = m_iDuration; + value["track"] = GetTrackNumber(); + value["disc"] = GetDiscNumber(); + value["loaded"] = m_bLoaded; + value["year"] = GetYear(); // Optionally from m_strOriginalDate + value["musicbrainztrackid"] = m_strMusicBrainzTrackID; + value["musicbrainzartistid"] = m_musicBrainzArtistID; + value["musicbrainzalbumid"] = m_strMusicBrainzAlbumID; + value["musicbrainzreleasegroupid"] = m_strMusicBrainzReleaseGroupID; + value["musicbrainzalbumartistid"] = m_musicBrainzAlbumArtistID; + value["comment"] = m_strComment; + value["contributors"] = CVariant(CVariant::VariantTypeArray); + for (const auto& role : m_musicRoles) + { + CVariant contributor; + contributor["name"] = role.GetArtist(); + contributor["role"] = role.GetRoleDesc(); + contributor["roleid"] = role.GetRoleId(); + contributor["artistid"] = (int)(role.GetArtistId()); + value["contributors"].push_back(contributor); + } + value["displaycomposer"] = GetArtistStringForRole("composer"); //TCOM + value["displayconductor"] = GetArtistStringForRole("conductor"); //TPE3 + value["displayorchestra"] = GetArtistStringForRole("orchestra"); + value["displaylyricist"] = GetArtistStringForRole("lyricist"); //TEXT + value["mood"] = StringUtils::Split(m_strMood, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + value["recordlabel"] = m_strRecordLabel; + value["rating"] = m_Rating; + value["userrating"] = m_Userrating; + value["votes"] = m_Votes; + value["playcount"] = m_iTimesPlayed; + value["lastplayed"] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; + value["dateadded"] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; + value["datenew"] = m_dateNew.IsValid() ? m_dateNew.GetAsDBDateTime() : StringUtils::Empty; + value["datemodified"] = + m_dateUpdated.IsValid() ? m_dateUpdated.GetAsDBDateTime() : StringUtils::Empty; + value["lyrics"] = m_strLyrics; + value["albumid"] = m_iAlbumId; + value["compilationartist"] = m_bCompilation; + value["compilation"] = m_bCompilation; + if (m_type.compare(MediaTypeAlbum) == 0) + value["releasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType); + else if (m_type.compare(MediaTypeSong) == 0) + value["albumreleasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType); + value["isboxset"] = m_bBoxset; + value["totaldiscs"] = m_iDiscTotal; + value["disctitle"] = m_strDiscSubtitle; + value["releasedate"] = m_strReleaseDate; + value["originaldate"] = m_strOriginalDate; + value["albumstatus"] = m_strReleaseStatus; + value["bpm"] = m_iBPM; + value["bitrate"] = m_bitrate; + value["samplerate"] = m_samplerate; + value["channels"] = m_channels; +} + +void CMusicInfoTag::ToSortable(SortItem& sortable, Field field) const +{ + switch (field) + { + case FieldTitle: + { + // make sure not to overwrite an existing path with an empty one + std::string title = m_strTitle; + if (!title.empty() || sortable.find(FieldTitle) == sortable.end()) + sortable[FieldTitle] = title; + break; + } + case FieldArtist: sortable[FieldArtist] = m_strArtistDesc; break; + case FieldArtistSort: sortable[FieldArtistSort] = m_strArtistSort; break; + case FieldAlbum: sortable[FieldAlbum] = m_strAlbum; break; + case FieldAlbumArtist: sortable[FieldAlbumArtist] = m_strAlbumArtistDesc; break; + case FieldGenre: sortable[FieldGenre] = m_genre; break; + case FieldTime: sortable[FieldTime] = m_iDuration; break; + case FieldTrackNumber: sortable[FieldTrackNumber] = m_iTrack; break; + case FieldTotalDiscs: + sortable[FieldTotalDiscs] = m_iDiscTotal; + break; + case FieldYear: + sortable[FieldYear] = GetYear(); // Optionally from m_strOriginalDate + break; + case FieldComment: sortable[FieldComment] = m_strComment; break; + case FieldMoods: sortable[FieldMoods] = m_strMood; break; + case FieldRating: sortable[FieldRating] = m_Rating; break; + case FieldUserRating: sortable[FieldUserRating] = m_Userrating; break; + case FieldVotes: sortable[FieldVotes] = m_Votes; break; + case FieldPlaycount: sortable[FieldPlaycount] = m_iTimesPlayed; break; + case FieldLastPlayed: sortable[FieldLastPlayed] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; break; + case FieldDateAdded: sortable[FieldDateAdded] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; break; + case FieldListeners: sortable[FieldListeners] = m_listeners; break; + case FieldId: sortable[FieldId] = (int64_t)m_iDbId; break; + case FieldOrigDate: sortable[FieldOrigDate] = m_strOriginalDate; break; + case FieldBPM: sortable[FieldBPM] = m_iBPM; break; + default: break; + } +} + +void CMusicInfoTag::Archive(CArchive& ar) +{ + if (ar.IsStoring()) + { + ar << m_strURL; + ar << m_strTitle; + ar << m_artist; + ar << m_strArtistSort; + ar << m_strArtistDesc; + ar << m_strAlbum; + ar << m_albumArtist; + ar << m_strAlbumArtistDesc; + ar << m_genre; + ar << m_iDuration; + ar << m_iTrack; + ar << m_bLoaded; + ar << m_strReleaseDate; + ar << m_strOriginalDate; + ar << m_strMusicBrainzTrackID; + ar << m_musicBrainzArtistID; + ar << m_strMusicBrainzAlbumID; + ar << m_strMusicBrainzReleaseGroupID; + ar << m_musicBrainzAlbumArtistID; + ar << m_strDiscSubtitle; + ar << m_bBoxset; + ar << m_iDiscTotal; + ar << m_strMusicBrainzReleaseType; + ar << m_lastPlayed; + ar << m_dateAdded; + ar << m_strComment; + ar << (int)m_musicRoles.size(); + for (const auto& credit : m_musicRoles) + { + ar << credit.GetRoleId(); + ar << credit.GetRoleDesc(); + ar << credit.GetArtist(); + ar << credit.GetArtistId(); + } + ar << m_strMood; + ar << m_strRecordLabel; + ar << m_Rating; + ar << m_Userrating; + ar << m_Votes; + ar << m_iTimesPlayed; + ar << m_iAlbumId; + ar << m_iDbId; + ar << m_type; + ar << m_strReleaseStatus; + ar << m_strLyrics; + ar << m_bCompilation; + ar << m_listeners; + ar << m_coverArt; + ar << m_cuesheet; + ar << static_cast<int>(m_albumReleaseType); + ar << m_iBPM; + ar << m_samplerate; + ar << m_bitrate; + ar << m_channels; + } + else + { + ar >> m_strURL; + ar >> m_strTitle; + ar >> m_artist; + ar >> m_strArtistSort; + ar >> m_strArtistDesc; + ar >> m_strAlbum; + ar >> m_albumArtist; + ar >> m_strAlbumArtistDesc; + ar >> m_genre; + ar >> m_iDuration; + ar >> m_iTrack; + ar >> m_bLoaded; + ar >> m_strReleaseDate; + ar >> m_strOriginalDate; + ar >> m_strMusicBrainzTrackID; + ar >> m_musicBrainzArtistID; + ar >> m_strMusicBrainzAlbumID; + ar >> m_strMusicBrainzReleaseGroupID; + ar >> m_musicBrainzAlbumArtistID; + ar >> m_strDiscSubtitle; + ar >> m_bBoxset; + ar >> m_iDiscTotal; + ar >> m_strMusicBrainzReleaseType; + ar >> m_lastPlayed; + ar >> m_dateAdded; + ar >> m_strComment; + int iMusicRolesSize; + ar >> iMusicRolesSize; + m_musicRoles.reserve(iMusicRolesSize); + for (int i = 0; i < iMusicRolesSize; ++i) + { + int idRole; + int idArtist; + std::string strArtist; + std::string strRole; + ar >> idRole; + ar >> strRole; + ar >> strArtist; + ar >> idArtist; + m_musicRoles.emplace_back(idRole, strRole, strArtist, idArtist); + } + ar >> m_strMood; + ar >> m_strRecordLabel; + ar >> m_Rating; + ar >> m_Userrating; + ar >> m_Votes; + ar >> m_iTimesPlayed; + ar >> m_iAlbumId; + ar >> m_iDbId; + ar >> m_type; + ar >> m_strReleaseStatus; + ar >> m_strLyrics; + ar >> m_bCompilation; + ar >> m_listeners; + ar >> m_coverArt; + ar >> m_cuesheet; + + int albumReleaseType; + ar >> albumReleaseType; + m_albumReleaseType = static_cast<CAlbum::ReleaseType>(albumReleaseType); + ar >> m_iBPM; + ar >> m_samplerate; + ar >> m_bitrate; + ar >> m_channels; + } +} + +void CMusicInfoTag::Clear() +{ + m_strURL.clear(); + m_artist.clear(); + m_strArtistSort.clear(); + m_strComposerSort.clear(); + m_strAlbum.clear(); + m_albumArtist.clear(); + m_genre.clear(); + m_strTitle.clear(); + m_strMusicBrainzTrackID.clear(); + m_musicBrainzArtistID.clear(); + m_strMusicBrainzAlbumID.clear(); + m_strMusicBrainzReleaseGroupID.clear(); + m_musicBrainzAlbumArtistID.clear(); + m_strMusicBrainzReleaseType.clear(); + m_musicRoles.clear(); + m_iDuration = 0; + m_iTrack = 0; + m_bLoaded = false; + m_lastPlayed.Reset(); + m_dateAdded.Reset(); + m_dateNew.Reset(); + m_dateUpdated.Reset(); + m_bCompilation = false; + m_bBoxset = false; + m_strDiscSubtitle.clear(); + m_strComment.clear(); + m_strMood.clear(); + m_strRecordLabel.clear(); + m_cuesheet.clear(); + m_iDbId = -1; + m_type.clear(); + m_strReleaseStatus.clear(); + m_iTimesPlayed = 0; + m_strReleaseDate.clear(); + m_strOriginalDate.clear(); + m_iAlbumId = -1; + m_coverArt.Clear(); + m_replayGain = ReplayGain(); + m_albumReleaseType = CAlbum::Album; + m_listeners = 0; + m_Rating = 0; + m_Userrating = 0; + m_Votes = 0; + m_iDiscTotal = 0; + m_iBPM = 0; + m_samplerate = 0; + m_bitrate = 0; + m_channels = 0; + m_stationName.clear(); + m_stationArt.clear(); +} + +void CMusicInfoTag::AppendArtist(const std::string &artist) +{ + for (unsigned int index = 0; index < m_artist.size(); index++) + { + if (StringUtils::EqualsNoCase(artist, m_artist.at(index))) + return; + } + + m_artist.push_back(artist); +} + +void CMusicInfoTag::AppendAlbumArtist(const std::string &albumArtist) +{ + for (unsigned int index = 0; index < m_albumArtist.size(); index++) + { + if (StringUtils::EqualsNoCase(albumArtist, m_albumArtist.at(index))) + return; + } + + m_albumArtist.push_back(albumArtist); +} + +void CMusicInfoTag::AppendGenre(const std::string &genre) +{ + for (unsigned int index = 0; index < m_genre.size(); index++) + { + if (StringUtils::EqualsNoCase(genre, m_genre.at(index))) + return; + } + + m_genre.push_back(genre); +} + +void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::string& strArtist) +{ + if (!strArtist.empty() && !Role.empty()) + AddArtistRole(Role, StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::vector<std::string>& artists) +{ + for (unsigned int index = 0; index < artists.size(); index++) + { + CMusicRole ArtistCredit(Role, Trim(artists.at(index))); + //Prevent duplicate entries + auto credit = find(m_musicRoles.begin(), m_musicRoles.end(), ArtistCredit); + if (credit == m_musicRoles.end()) + m_musicRoles.push_back(ArtistCredit); + } +} + +void CMusicInfoTag::AppendArtistRole(const CMusicRole& ArtistRole) +{ + //Append contributor, no check for duplicates as from database + m_musicRoles.push_back(ArtistRole); +} + +const std::string CMusicInfoTag::GetArtistStringForRole(const std::string& strRole) const +{ + std::vector<std::string> artistvector; + for (const auto& credit : m_musicRoles) + { + if (StringUtils::EqualsNoCase(credit.GetRoleDesc(), strRole)) + artistvector.push_back(credit.GetArtist()); + } + return StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); +} + +const std::string CMusicInfoTag::GetContributorsText() const +{ + std::string strLabel; + for (const auto& credit : m_musicRoles) + { + strLabel += StringUtils::Format("{}\n", credit.GetArtist()); + } + return StringUtils::TrimRight(strLabel, "\n"); +} + +const std::string CMusicInfoTag::GetContributorsAndRolesText() const +{ + std::string strLabel; + for (const auto& credit : m_musicRoles) + { + strLabel += StringUtils::Format("{} - {}\n", credit.GetRoleDesc(), credit.GetArtist()); + } + return StringUtils::TrimRight(strLabel, "\n"); +} + + +const VECMUSICROLES &CMusicInfoTag::GetContributors() const +{ + return m_musicRoles; +} + +void CMusicInfoTag::SetContributors(const VECMUSICROLES& contributors) +{ + m_musicRoles = contributors; +} + +std::string CMusicInfoTag::Trim(const std::string &value) const +{ + std::string trimmedValue(value); + StringUtils::TrimLeft(trimmedValue, " "); + StringUtils::TrimRight(trimmedValue, " \n\r"); + return trimmedValue; +} diff --git a/xbmc/music/tags/MusicInfoTag.h b/xbmc/music/tags/MusicInfoTag.h new file mode 100644 index 0000000..3c1b994 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTag.h @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +class CSong; +class CArtist; +class CVariant; + +#include "ReplayGain.h" +#include "XBDateTime.h" +#include "music/Album.h" +#include "utils/IArchivable.h" +#include "utils/ISerializable.h" +#include "utils/ISortable.h" + +#include <string> +#include <vector> + +namespace MUSIC_INFO +{ +class CMusicInfoTag final : public IArchivable, public ISerializable, public ISortable +{ +public: + CMusicInfoTag(void); + bool operator !=(const CMusicInfoTag& tag) const; + bool Loaded() const; + const std::string& GetTitle() const; + const std::string& GetURL() const; + const std::vector<std::string>& GetArtist() const; + const std::string& GetArtistSort() const; + const std::string GetArtistString() const; + const std::string& GetComposerSort() const; + const std::string& GetAlbum() const; + int GetAlbumId() const; + const std::vector<std::string>& GetAlbumArtist() const; + const std::string GetAlbumArtistString() const; + const std::string& GetAlbumArtistSort() const; + const std::vector<std::string>& GetGenre() const; + int GetTrackNumber() const; + int GetDiscNumber() const; + int GetTrackAndDiscNumber() const; + int GetTotalDiscs() const; + int GetDuration() const; // may be set even if Loaded() returns false + int GetYear() const; + const std::string& GetReleaseDate() const; + const std::string GetReleaseYear() const; + const std::string& GetOriginalDate() const; + const std::string GetOriginalYear() const; + int GetDatabaseId() const; + const std::string &GetType() const; + const std::string& GetDiscSubtitle() const; + int GetBPM() const; + std::string GetYearString() const; + const std::string& GetMusicBrainzTrackID() const; + const std::vector<std::string>& GetMusicBrainzArtistID() const; + const std::vector<std::string>& GetMusicBrainzArtistHints() const; + const std::string& GetMusicBrainzAlbumID() const; + const std::string& GetMusicBrainzReleaseGroupID() const; + const std::vector<std::string>& GetMusicBrainzAlbumArtistID() const; + const std::vector<std::string>& GetMusicBrainzAlbumArtistHints() const; + const std::string& GetMusicBrainzReleaseType() const; + const std::string& GetComment() const; + const std::string& GetMood() const; + const std::string& GetRecordLabel() const; + const std::string& GetLyrics() const; + const std::string& GetCueSheet() const; + const CDateTime& GetLastPlayed() const; + const CDateTime& GetDateAdded() const; + bool GetCompilation() const; + bool GetBoxset() const; + float GetRating() const; + int GetUserrating() const; + int GetVotes() const; + int GetListeners() const; + int GetPlayCount() const; + int GetBitRate() const; + int GetNoOfChannels() const; + int GetSampleRate() const; + const std::string& GetAlbumReleaseStatus() const; + const std::string& GetStationName() const; + const std::string& GetStationArt() const; + const EmbeddedArtInfo &GetCoverArtInfo() const; + const ReplayGain& GetReplayGain() const; + CAlbum::ReleaseType GetAlbumReleaseType() const; + + void SetURL(const std::string& strURL); + void SetTitle(const std::string& strTitle); + void SetArtist(const std::string& strArtist); + void SetArtist(const std::vector<std::string>& artists, bool FillDesc = false); + void SetArtistDesc(const std::string& strArtistDesc); + void SetArtistSort(const std::string& strArtistsort); + void SetComposerSort(const std::string& strComposerSort); + void SetAlbum(const std::string& strAlbum); + void SetAlbumId(const int iAlbumId); + void SetAlbumArtist(const std::string& strAlbumArtist); + void SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc = false); + void SetAlbumArtistDesc(const std::string& strAlbumArtistDesc); + void SetAlbumArtistSort(const std::string& strAlbumArtistSort); + void SetGenre(const std::string& strGenre, bool bTrim = false); + void SetGenre(const std::vector<std::string>& genres, bool bTrim = false); + void SetYear(int year); + void SetOriginalDate(const std::string& strOriginalDate); + void SetReleaseDate(const std::string& strReleaseDate); + void SetDatabaseId(int id, const std::string &type); + void SetTrackNumber(int iTrack); + void SetDiscNumber(int iDiscNumber); + void SetTrackAndDiscNumber(int iTrackAndDisc); + void SetDuration(int iSec); + void SetLoaded(bool bOnOff = true); + void SetArtist(const CArtist& artist); + void SetAlbum(const CAlbum& album); + void SetSong(const CSong& song); + void SetMusicBrainzTrackID(const std::string& strTrackID); + void SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId); + void SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints); + void SetMusicBrainzAlbumID(const std::string& strAlbumID); + void SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId); + void SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints); + void SetMusicBrainzReleaseGroupID(const std::string& strReleaseGroupID); + void SetMusicBrainzReleaseType(const std::string& ReleaseType); + void SetComment(const std::string& comment); + void SetMood(const std::string& mood); + void SetRecordLabel(const std::string& publisher); + void SetLyrics(const std::string& lyrics); + void SetCueSheet(const std::string& cueSheet); + void SetRating(float rating); + void SetUserrating(int rating); + void SetVotes(int votes); + void SetListeners(int listeners); + void SetPlayCount(int playcount); + void SetLastPlayed(const std::string& strLastPlayed); + void SetLastPlayed(const CDateTime& strLastPlayed); + void SetDateAdded(const std::string& strDateAdded); + void SetDateAdded(const CDateTime& dateAdded); + void SetDateUpdated(const std::string& strDateUpdated); + void SetDateUpdated(const CDateTime& dateUpdated); + void SetDateNew(const std::string& strDateNew); + void SetDateNew(const CDateTime& dateNew); + void SetCompilation(bool compilation); + void SetBoxset(bool boxset); + void SetCoverArtInfo(size_t size, const std::string &mimeType); + void SetReplayGain(const ReplayGain& aGain); + void SetAlbumReleaseType(CAlbum::ReleaseType releaseType); + void SetType(const MediaType& mediaType); + void SetDiscSubtitle(const std::string& strDiscSubtitle); + void SetTotalDiscs(int iDiscTotal); + void SetBPM(int iBPM); + void SetBitRate(int bitrate); + void SetNoOfChannels(int channels); + void SetSampleRate(int samplerate); + void SetAlbumReleaseStatus(const std::string& strReleaseStatus); + void SetStationName(const std::string& strStationName); // name of online radio station + void SetStationArt(const std::string& strStationArt); + + /*! \brief Append a unique artist to the artist list + Checks if we have this artist already added, and if not adds it to the songs artist list. + \param value artist to add. + */ + void AppendArtist(const std::string &artist); + + /*! \brief Append a unique album artist to the artist list + Checks if we have this album artist already added, and if not adds it to the songs album artist list. + \param albumArtist album artist to add. + */ + void AppendAlbumArtist(const std::string &albumArtist); + + /*! \brief Append a unique genre to the genre list + Checks if we have this genre already added, and if not adds it to the songs genre list. + \param genre genre to add. + */ + void AppendGenre(const std::string &genre); + void AddOriginalDate(const std::string& strDateYear); + void AddReleaseDate(const std::string& strDateYear, bool isMonth = false); + + void AddArtistRole(const std::string& Role, const std::string& strArtist); + void AddArtistRole(const std::string& Role, const std::vector<std::string>& artists); + void AppendArtistRole(const CMusicRole& ArtistRole); + const std::string GetArtistStringForRole(const std::string& strRole) const; + const std::string GetContributorsText() const; + const std::string GetContributorsAndRolesText() const; + const VECMUSICROLES &GetContributors() const; + void SetContributors(const VECMUSICROLES& contributors); + bool HasContributors() const { return !m_musicRoles.empty(); } + + void Archive(CArchive& ar) override; + void Serialize(CVariant& ar) const override; + void ToSortable(SortItem& sortable, Field field) const override; + + void Clear(); + +protected: + /*! \brief Trim whitespace off the given string + \param value string to trim + \return trimmed value, with spaces removed from left and right, as well as carriage returns from the right. + */ + std::string Trim(const std::string &value) const; + + std::string m_strURL; + std::string m_strTitle; + std::vector<std::string> m_artist; + std::string m_strArtistSort; + std::string m_strArtistDesc; + std::string m_strComposerSort; + std::string m_strAlbum; + std::vector<std::string> m_albumArtist; + std::string m_strAlbumArtistDesc; + std::string m_strAlbumArtistSort; + std::vector<std::string> m_genre; + std::string m_strMusicBrainzTrackID; + std::vector<std::string> m_musicBrainzArtistID; + std::vector<std::string> m_musicBrainzArtistHints; + std::string m_strMusicBrainzAlbumID; + std::vector<std::string> m_musicBrainzAlbumArtistID; + std::vector<std::string> m_musicBrainzAlbumArtistHints; + std::string m_strMusicBrainzReleaseGroupID; + std::string m_strMusicBrainzReleaseType; + VECMUSICROLES m_musicRoles; //Artists contributing to the recording and role (from tags other than ARTIST or ALBUMARTIST) + std::string m_strComment; + std::string m_strMood; + std::string m_strRecordLabel; + std::string m_strLyrics; + std::string m_cuesheet; + std::string m_strDiscSubtitle; + std::string m_strReleaseDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD + std::string m_strOriginalDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD + CDateTime m_lastPlayed; + CDateTime m_dateNew; + CDateTime m_dateAdded; + CDateTime m_dateUpdated; + bool m_bCompilation; + int m_iDuration; + int m_iTrack; // consists of the disk number in the high 16 bits, the track number in the low 16bits + int m_iDbId; + MediaType m_type; ///< item type "music", "song", "album", "artist" + bool m_bLoaded; + float m_Rating; + int m_Userrating; + int m_Votes; + int m_listeners; + int m_iTimesPlayed; + int m_iAlbumId; + int m_iDiscTotal; + bool m_bBoxset; + int m_iBPM; + CAlbum::ReleaseType m_albumReleaseType; + std::string m_strReleaseStatus; + int m_samplerate; + int m_channels; + int m_bitrate; + std::string m_stationName; + std::string m_stationArt; // Used to fetch thumb URL for Shoutcasts + + EmbeddedArtInfo m_coverArt; ///< art information + + ReplayGain m_replayGain; ///< ReplayGain information +}; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp new file mode 100644 index 0000000..66938d3 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTagLoaderCDDA.h" + +#include "MusicInfoTag.h" +#include "ServiceBroker.h" +#include "network/cddb.h" +#include "profiles/ProfileManager.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/log.h" + +using namespace MUSIC_INFO; + +#ifdef HAS_DVD_DRIVE +using namespace MEDIA_DETECT; +using namespace CDDB; +#endif + +//! @todo - remove after Ubuntu 16.04 (Xenial) is EOL +#if !defined(LIBCDIO_VERSION_NUM) || (LIBCDIO_VERSION_NUM <= 83) +#define CDTEXT_FIELD_TITLE CDTEXT_TITLE +#define CDTEXT_FIELD_PERFORMER CDTEXT_PERFORMER +#define CDTEXT_FIELD_GENRE CDTEXT_GENRE +#endif + +CMusicInfoTagLoaderCDDA::CMusicInfoTagLoaderCDDA(void) = default; + +CMusicInfoTagLoaderCDDA::~CMusicInfoTagLoaderCDDA() = default; + +bool CMusicInfoTagLoaderCDDA::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art) +{ +#ifdef HAS_DVD_DRIVE + try + { + tag.SetURL(strFileName); + bool bResult = false; + + // Get information for the inserted disc + CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(); + if (pCdInfo == NULL) + return bResult; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // Prepare cddb + Xcddb cddb; + cddb.setCacheDir(profileManager->GetCDDBFolder()); + + int iTrack = atoi(strFileName.substr(13, strFileName.size() - 13 - 5).c_str()); + + // duration is always available + tag.SetDuration( ( pCdInfo->GetTrackInformation(iTrack).nMins * 60 ) + + pCdInfo->GetTrackInformation(iTrack).nSecs ); + + // Only load cached cddb info in this tag loader, the internet database query is made in CCDDADirectory + if (pCdInfo->HasCDDBInfo() && cddb.isCDCached(pCdInfo)) + { + // get cddb information + if (cddb.queryCDinfo(pCdInfo)) + { + // Fill the fileitems music tag with cddb information, if available + const std::string& strTitle = cddb.getTrackTitle(iTrack); + if (!strTitle.empty()) + { + // Tracknumber + tag.SetTrackNumber(iTrack); + + // Title + tag.SetTitle(strTitle); + + // Artist: Use track artist or disc artist + std::string strArtist = cddb.getTrackArtist(iTrack); + if (strArtist.empty()) + cddb.getDiskArtist(strArtist); + tag.SetArtist(strArtist); + + // Album + std::string strAlbum; + cddb.getDiskTitle( strAlbum ); + tag.SetAlbum(strAlbum); + + // Album Artist + std::string strAlbumArtist; + cddb.getDiskArtist(strAlbumArtist); + tag.SetAlbumArtist(strAlbumArtist); + + // Year + tag.SetReleaseDate(cddb.getYear()); + + // Genre + tag.SetGenre( cddb.getGenre() ); + + tag.SetLoaded(true); + bResult = true; + } + } + } + else + { + // No cddb info, maybe we have CD-Text + trackinfo ti = pCdInfo->GetTrackInformation(iTrack); + + // Fill the fileitems music tag with CD-Text information, if available + std::string strTitle = ti.cdtext[CDTEXT_FIELD_TITLE]; + if (!strTitle.empty()) + { + // Tracknumber + tag.SetTrackNumber(iTrack); + + // Title + tag.SetTitle(strTitle); + + // Get info for track zero, as we may have and need CD-Text Album info + xbmc_cdtext_t discCDText = pCdInfo->GetDiscCDTextInformation(); + + // Artist: Use track artist or disc artist + std::string strArtist = ti.cdtext[CDTEXT_FIELD_PERFORMER]; + if (strArtist.empty()) + strArtist = discCDText[CDTEXT_FIELD_PERFORMER]; + tag.SetArtist(strArtist); + + // Album + std::string strAlbum; + strAlbum = discCDText[CDTEXT_FIELD_TITLE]; + tag.SetAlbum(strAlbum); + + // Genre: use track or disc genre + std::string strGenre = ti.cdtext[CDTEXT_FIELD_GENRE]; + if (strGenre.empty()) + strGenre = discCDText[CDTEXT_FIELD_GENRE]; + tag.SetGenre( strGenre ); + + tag.SetLoaded(true); + bResult = true; + } + } + return bResult; + } + catch (...) + { + CLog::Log(LOGERROR, "Tag loader CDDB: exception in file {}", strFileName); + } + +#endif + + tag.SetLoaded(false); + + return false; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.h b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h new file mode 100644 index 0000000..95fc35f --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +namespace MUSIC_INFO +{ + class CMusicInfoTagLoaderCDDA: public IMusicInfoTagLoader + { + public: + CMusicInfoTagLoaderCDDA(void); + ~CMusicInfoTagLoaderCDDA() override; + + bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override; + }; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp new file mode 100644 index 0000000..1409809 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTagLoaderDatabase.h" + +#include "MusicInfoTag.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h" +#include "music/MusicDatabase.h" + +using namespace MUSIC_INFO; + +CMusicInfoTagLoaderDatabase::CMusicInfoTagLoaderDatabase(void) = default; + +CMusicInfoTagLoaderDatabase::~CMusicInfoTagLoaderDatabase() = default; + +bool CMusicInfoTagLoaderDatabase::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art) +{ + tag.SetLoaded(false); + CMusicDatabase database; + database.Open(); + XFILE::MUSICDATABASEDIRECTORY::CQueryParams param; + XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strFileName,param); + + CSong song; + if (database.GetSong(param.GetSongId(),song)) + tag.SetSong(song); + + database.Close(); + + return tag.Loaded(); +} + diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.h b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h new file mode 100644 index 0000000..85272e5 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +namespace MUSIC_INFO +{ + class CMusicInfoTagLoaderDatabase: public IMusicInfoTagLoader + { + public: + CMusicInfoTagLoaderDatabase(void); + ~CMusicInfoTagLoaderDatabase() override; + + bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override; + }; +} + diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp new file mode 100644 index 0000000..9bb2cdc --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTagLoaderFFmpeg.h" + +#include "MusicInfoTag.h" +#include "cores/FFmpeg.h" +#include "filesystem/File.h" +#include "utils/StringUtils.h" + +using namespace MUSIC_INFO; +using namespace XFILE; + +static int vfs_file_read(void *h, uint8_t* buf, int size) +{ + CFile* pFile = static_cast<CFile*>(h); + return pFile->Read(buf, size); +} + +static int64_t vfs_file_seek(void *h, int64_t pos, int whence) +{ + CFile* pFile = static_cast<CFile*>(h); + if (whence == AVSEEK_SIZE) + return pFile->GetLength(); + else + return pFile->Seek(pos, whence & ~AVSEEK_FORCE); +} + +CMusicInfoTagLoaderFFmpeg::CMusicInfoTagLoaderFFmpeg(void) = default; + +CMusicInfoTagLoaderFFmpeg::~CMusicInfoTagLoaderFFmpeg() = default; + +bool CMusicInfoTagLoaderFFmpeg::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art) +{ + tag.SetLoaded(false); + + CFile file; + if (!file.Open(strFileName)) + return false; + + int bufferSize = 4096; + int blockSize = file.GetChunkSize(); + if (blockSize > 1) + bufferSize = blockSize; + uint8_t* buffer = (uint8_t*)av_malloc(bufferSize); + AVIOContext* ioctx = avio_alloc_context(buffer, bufferSize, 0, + &file, vfs_file_read, NULL, + vfs_file_seek); + + AVFormatContext* fctx = avformat_alloc_context(); + fctx->pb = ioctx; + + if (file.IoControl(IOCTRL_SEEK_POSSIBLE, NULL) != 1) + ioctx->seekable = 0; + + AVInputFormat* iformat=NULL; + av_probe_input_buffer(ioctx, &iformat, strFileName.c_str(), NULL, 0, 0); + + if (avformat_open_input(&fctx, strFileName.c_str(), iformat, NULL) < 0) + { + if (fctx) + avformat_close_input(&fctx); + av_free(ioctx->buffer); + av_free(ioctx); + return false; + } + + /* ffmpeg supports the return of ID3v2 metadata but has its own naming system + for some, but not all, of the keys. In particular the key for the conductor + tag TPE3 is called "performer". + See https://github.com/xbmc/FFmpeg/blob/master/libavformat/id3v2.c#L43 + Other keys are retuened using their 4 char name. + Only single frame values are returned even for v2.4 fomart tags e.g. while + tagged with multiple TPE1 frames "artist1", "artist2", "artist3" only + "artist1" is returned by ffmpeg. + Hence, like with v2.3 format tags, multiple values for artist, genre etc. + need to be combined when tagging into a single value using a known item + separator e.g. "artist1 / artist2 / artist3" + + Any changes to ID3v2 tag processing in CTagLoaderTagLib need to be + repeated here + */ + auto&& ParseTag = [&tag](AVDictionaryEntry* avtag) + { + if (StringUtils::CompareNoCase(avtag->key, "album") == 0) + tag.SetAlbum(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "artist") == 0) + tag.SetArtist(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "album_artist") == 0 || + StringUtils::CompareNoCase(avtag->key, "album artist") == 0) + tag.SetAlbumArtist(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "title") == 0) + tag.SetTitle(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "genre") == 0) + tag.SetGenre(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "part_number") == 0 || + StringUtils::CompareNoCase(avtag->key, "track") == 0) + tag.SetTrackNumber( + static_cast<int>(strtol(avtag->value, nullptr, 10))); + else if (StringUtils::CompareNoCase(avtag->key, "disc") == 0) + tag.SetDiscNumber( + static_cast<int>(strtol(avtag->value, nullptr, 10))); + else if (StringUtils::CompareNoCase(avtag->key, "date") == 0) + tag.SetReleaseDate(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "compilation") == 0) + tag.SetCompilation((strtol(avtag->value, nullptr, 10) == 0) ? false : true); + else if (StringUtils::CompareNoCase(avtag->key, "encoded_by") == 0) {} + else if (StringUtils::CompareNoCase(avtag->key, "composer") == 0) + tag.AddArtistRole("Composer", avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "performer") == 0) // Conductor or TPE3 tag + tag.AddArtistRole("Conductor", avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "TEXT") == 0) + tag.AddArtistRole("Lyricist", avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "TPE4") == 0) + tag.AddArtistRole("Remixer", avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "LABEL") == 0 || + StringUtils::CompareNoCase(avtag->key, "TPUB") == 0) + tag.SetRecordLabel(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "copyright") == 0 || + StringUtils::CompareNoCase(avtag->key, "TCOP") == 0) {} // Copyright message + else if (StringUtils::CompareNoCase(avtag->key, "TDRC") == 0) + tag.SetReleaseDate(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "TDOR") == 0 || + StringUtils::CompareNoCase(avtag->key, "TORY") == 0) + tag.SetOriginalDate(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key , "TDAT") == 0) + tag.AddReleaseDate(avtag->value, true); // MMDD part + else if (StringUtils::CompareNoCase(avtag->key, "TYER") == 0) + tag.AddReleaseDate(avtag->value); // YYYY part + else if (StringUtils::CompareNoCase(avtag->key, "TBPM") == 0) + tag.SetBPM(static_cast<int>(strtol(avtag->value, nullptr, 10))); + else if (StringUtils::CompareNoCase(avtag->key, "TDTG") == 0) {} // Tagging time + else if (StringUtils::CompareNoCase(avtag->key, "language") == 0 || + StringUtils::CompareNoCase(avtag->key, "TLAN") == 0) {} // Languages + else if (StringUtils::CompareNoCase(avtag->key, "mood") == 0 || + StringUtils::CompareNoCase(avtag->key, "TMOO") == 0) + tag.SetMood(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "artist-sort") == 0 || + StringUtils::CompareNoCase(avtag->key, "TSOP") == 0) {} + else if (StringUtils::CompareNoCase(avtag->key, "TSO2") == 0) {} // Album artist sort + else if (StringUtils::CompareNoCase(avtag->key, "TSOC") == 0) {} // composer sort + else if (StringUtils::CompareNoCase(avtag->key, "TSST") == 0) + tag.SetDiscSubtitle(avtag->value); + }; + + AVDictionaryEntry* avtag=nullptr; + while ((avtag = av_dict_get(fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX))) + ParseTag(avtag); + + const AVStream* st = fctx->streams[0]; + if (st) + while ((avtag = av_dict_get(st->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX))) + ParseTag(avtag); + + if (!tag.GetTitle().empty()) + tag.SetLoaded(true); + + avformat_close_input(&fctx); + av_free(ioctx->buffer); + av_free(ioctx); + + return true; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h new file mode 100644 index 0000000..272c065 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +namespace MUSIC_INFO +{ + class CMusicInfoTagLoaderFFmpeg: public IMusicInfoTagLoader + { + public: + CMusicInfoTagLoaderFFmpeg(void); + ~CMusicInfoTagLoaderFFmpeg() override; + + bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override; + }; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp new file mode 100644 index 0000000..56b2c17 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTagLoaderFactory.h" + +#include "FileItem.h" +#include "MusicInfoTagLoaderCDDA.h" +#include "MusicInfoTagLoaderDatabase.h" +#include "MusicInfoTagLoaderFFmpeg.h" +#include "MusicInfoTagLoaderShn.h" +#include "ServiceBroker.h" +#include "TagLoaderTagLib.h" +#include "addons/AudioDecoder.h" +#include "addons/ExtsMimeSupportList.h" +#include "addons/addoninfo/AddonType.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace KODI::ADDONS; +using namespace MUSIC_INFO; + +CMusicInfoTagLoaderFactory::CMusicInfoTagLoaderFactory() = default; + +CMusicInfoTagLoaderFactory::~CMusicInfoTagLoaderFactory() = default; + +IMusicInfoTagLoader* CMusicInfoTagLoaderFactory::CreateLoader(const CFileItem& item) +{ + // dont try to read the tags for streams & shoutcast + if (item.IsInternetStream()) + return NULL; + + if (item.IsMusicDb()) + return new CMusicInfoTagLoaderDatabase(); + + std::string strExtension = URIUtils::GetExtension(item.GetPath()); + StringUtils::ToLower(strExtension); + StringUtils::TrimLeft(strExtension, "."); + + if (strExtension.empty()) + return NULL; + + const auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos( + "." + strExtension, CExtsMimeSupportList::FilterSelect::hasTags); + for (const auto& addonInfo : addonInfos) + { + if (addonInfo.first == ADDON::AddonType::AUDIODECODER) + { + std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second); + if (!result->CreateDecoder() && result->SupportsFile(item.GetPath())) + continue; + + return result.release(); + } + } + + if (strExtension == "aac" || strExtension == "ape" || strExtension == "mac" || + strExtension == "mp3" || strExtension == "wma" || strExtension == "flac" || + strExtension == "m4a" || strExtension == "mp4" || strExtension == "m4b" || + strExtension == "m4v" || strExtension == "mpc" || strExtension == "mpp" || + strExtension == "mp+" || strExtension == "ogg" || strExtension == "oga" || + strExtension == "opus" || strExtension == "aif" || strExtension == "aiff" || + strExtension == "wav" || strExtension == "mod" || strExtension == "s3m" || + strExtension == "it" || strExtension == "xm" || strExtension == "wv") + { + CTagLoaderTagLib *pTagLoader = new CTagLoaderTagLib(); + return pTagLoader; + } +#ifdef HAS_DVD_DRIVE + else if (strExtension == "cdda") + { + CMusicInfoTagLoaderCDDA *pTagLoader = new CMusicInfoTagLoaderCDDA(); + return pTagLoader; + } +#endif + else if (strExtension == "shn") + { + CMusicInfoTagLoaderSHN *pTagLoader = new CMusicInfoTagLoaderSHN(); + return pTagLoader; + } + else if (strExtension == "mka" || strExtension == "dsf" || + strExtension == "dff") + return new CMusicInfoTagLoaderFFmpeg(); + + return NULL; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.h b/xbmc/music/tags/MusicInfoTagLoaderFactory.h new file mode 100644 index 0000000..ba2aba2 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +class CFileItem; // forward + +namespace MUSIC_INFO +{ + class CMusicInfoTagLoaderFactory + { + public: + CMusicInfoTagLoaderFactory(void); + virtual ~CMusicInfoTagLoaderFactory(); + + static IMusicInfoTagLoader* CreateLoader(const CFileItem& item); + }; +} + diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.cpp b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp new file mode 100644 index 0000000..2b9bd18 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicInfoTagLoaderShn.h" + +#include "MusicInfoTag.h" +#include "utils/log.h" + +using namespace MUSIC_INFO; + +CMusicInfoTagLoaderSHN::CMusicInfoTagLoaderSHN(void) = default; + +CMusicInfoTagLoaderSHN::~CMusicInfoTagLoaderSHN() = default; + +bool CMusicInfoTagLoaderSHN::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art) +{ + try + { + + tag.SetURL(strFileName); + tag.SetDuration((long)0); //! @todo Use libavformat to calculate duration. + tag.SetLoaded(false); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "Tag loader shn: exception in file {}", strFileName); + } + + tag.SetLoaded(false); + return false; +} diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.h b/xbmc/music/tags/MusicInfoTagLoaderShn.h new file mode 100644 index 0000000..d932320 --- /dev/null +++ b/xbmc/music/tags/MusicInfoTagLoaderShn.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +namespace MUSIC_INFO +{ + +class CMusicInfoTagLoaderSHN: public IMusicInfoTagLoader +{ +public: + CMusicInfoTagLoaderSHN(void); + ~CMusicInfoTagLoaderSHN() override; + + bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override; +}; +} diff --git a/xbmc/music/tags/ReplayGain.cpp b/xbmc/music/tags/ReplayGain.cpp new file mode 100644 index 0000000..c9947f4 --- /dev/null +++ b/xbmc/music/tags/ReplayGain.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ReplayGain.h" + +#include "utils/StringUtils.h" + +#include <stdlib.h> + +static bool TypeIsValid(ReplayGain::Type aType) +{ + return (aType > ReplayGain::NONE && aType <= ReplayGain::TRACK); +} + +static int index(ReplayGain::Type aType) +{ + return static_cast<int>(aType) - 1; +} + +/////////////////////////////////////////////////////////////// +// class ReplayGain +/////////////////////////////////////////////////////////////// + +const ReplayGain::Info& ReplayGain::Get(Type aType) const +{ + if (TypeIsValid(aType)) + return m_data[index(aType)]; + + static Info invalid; + return invalid; +} + +void ReplayGain::Set(Type aType, const Info& aInfo) +{ + if (TypeIsValid(aType)) + m_data[index(aType)] = aInfo; +} + +void ReplayGain::ParseGain(Type aType, const std::string& aStrGain) +{ + if (TypeIsValid(aType)) + m_data[index(aType)].SetGain(aStrGain); +} + +void ReplayGain::SetGain(Type aType, float aGain) +{ + if (TypeIsValid(aType)) + m_data[index(aType)].SetGain(aGain); +} + +void ReplayGain::ParsePeak(Type aType, const std::string& aStrPeak) +{ + if (TypeIsValid(aType)) + m_data[index(aType)].SetPeak(aStrPeak); +} + +void ReplayGain::SetPeak(Type aType, float aPeak) +{ + if (TypeIsValid(aType)) + m_data[index(aType)].SetPeak(aPeak); +} + +std::string ReplayGain::Get() const +{ + if (!Get(ALBUM).Valid() && !Get(TRACK).Valid()) + return std::string(); + + std::string rg; + if (Get(ALBUM).Valid()) + rg = StringUtils::Format("{:.3f},{:.3f},", Get(ALBUM).Gain(), Get(ALBUM).Peak()); + else + rg = "-1000, -1,"; + if (Get(TRACK).Valid()) + rg += StringUtils::Format("{:.3f},{:.3f}", Get(TRACK).Gain(), Get(TRACK).Peak()); + else + rg += "-1000, -1"; + return rg; +} + +void ReplayGain::Set(const std::string& strReplayGain) +{ + std::vector<std::string> values = StringUtils::Split(strReplayGain, ","); + if (values.size() == 4) + { + ParseGain(ALBUM, values[0]); + ParsePeak(ALBUM, values[1]); + ParseGain(TRACK, values[2]); + ParsePeak(TRACK, values[3]); + } +} + +/////////////////////////////////////////////////////////////// +// class ReplayGain::Info +/////////////////////////////////////////////////////////////// + +void ReplayGain::Info::SetGain(float aGain) +{ + m_gain = aGain; +} + +void ReplayGain::Info::SetGain(const std::string& aStrGain) +{ + SetGain(static_cast<float>(atof(aStrGain.c_str()))); +} + +float ReplayGain::Info::Gain() const +{ + return m_gain; +} + +void ReplayGain::Info::SetPeak(const std::string& aStrPeak) +{ + SetPeak(static_cast<float>(atof(aStrPeak.c_str()))); +} + +void ReplayGain::Info::SetPeak(float aPeak) +{ + m_peak = aPeak; +} + +float ReplayGain::Info::Peak() const +{ + return m_peak; +} + +bool ReplayGain::Info::HasGain() const +{ + return m_gain != REPLAY_GAIN_NO_GAIN; +} + +bool ReplayGain::Info::HasPeak() const +{ + return m_peak != REPLAY_GAIN_NO_PEAK; +} + +bool ReplayGain::Info::Valid() const +{ + return HasPeak() && HasGain(); +} diff --git a/xbmc/music/tags/ReplayGain.h b/xbmc/music/tags/ReplayGain.h new file mode 100644 index 0000000..efcd784 --- /dev/null +++ b/xbmc/music/tags/ReplayGain.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <string> + +#define REPLAY_GAIN_NO_PEAK -1.0f +#define REPLAY_GAIN_NO_GAIN -1000.0f + +class ReplayGain +{ +public: + enum Type { + NONE = 0, + ALBUM, + TRACK + }; +public: + class Info + { + public: + void SetGain(float aGain); + void SetGain(const std::string& aStrGain); + float Gain() const; + void SetPeak(const std::string& aStrPeak); + void SetPeak(float aPeak); + float Peak() const; + bool HasGain() const; + bool HasPeak() const; + bool Valid() const; + private: + float m_gain = REPLAY_GAIN_NO_GAIN; // measured in milliBels + float m_peak = REPLAY_GAIN_NO_PEAK; // 1.0 == full digital scale + }; + const Info& Get(Type aType) const; + void Set(Type aType, const Info& aInfo); + void ParseGain(Type aType, const std::string& aStrGain); + void SetGain(Type aType, float aGain); + void ParsePeak(Type aType, const std::string& aStrPeak); + void SetPeak(Type aType, float aPeak); + std::string Get() const; + void Set(const std::string& strReplayGain); +private: + Info m_data[TRACK]; +}; diff --git a/xbmc/music/tags/TagLibVFSStream.cpp b/xbmc/music/tags/TagLibVFSStream.cpp new file mode 100644 index 0000000..1d2b454 --- /dev/null +++ b/xbmc/music/tags/TagLibVFSStream.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "TagLibVFSStream.h" + +#include "filesystem/File.h" + +#include <limits.h> + +#include <taglib/tiostream.h> + +using namespace XFILE; +using namespace TagLib; +using namespace MUSIC_INFO; + +/*! + * Construct a File object and opens the \a file. \a file should be a + * be an XBMC Vfile. + */ +TagLibVFSStream::TagLibVFSStream(const std::string& strFileName, bool readOnly) +{ + m_bIsOpen = true; + if (readOnly) + { + if (!m_file.Open(strFileName)) + m_bIsOpen = false; + } + else + { + if (!m_file.OpenForWrite(strFileName)) + m_bIsOpen = false; + } + m_strFileName = strFileName; + m_bIsReadOnly = readOnly || !m_bIsOpen; +} + +/*! + * Destroys this ByteVectorStream instance. + */ +TagLibVFSStream::~TagLibVFSStream() +{ + m_file.Close(); +} + +/*! + * Returns the file name in the local file system encoding. + */ +FileName TagLibVFSStream::name() const +{ + return m_strFileName.c_str(); +} + +/*! + * Reads a block of size \a length at the current get pointer. + */ +ByteVector TagLibVFSStream::readBlock(TagLib::ulong length) +{ + ByteVector byteVector(static_cast<TagLib::uint>(length)); + ssize_t read = m_file.Read(byteVector.data(), length); + if (read > 0) + byteVector.resize(read); + else + byteVector.clear(); + + return byteVector; +} + +/*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ +void TagLibVFSStream::writeBlock(const ByteVector &data) +{ + m_file.Write(data.data(), data.size()); +} + +/*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ +void TagLibVFSStream::insert(const ByteVector &data, TagLib::ulong start, TagLib::ulong replace) +{ + if (data.size() == replace) + { + seek(start); + writeBlock(data); + return; + } + else if (data.size() < replace) + { + seek(start); + writeBlock(data); + removeBlock(start + data.size(), replace - data.size()); + } + + // Woohoo! Faster (about 20%) than id3lib at last. I had to get hardcore + // and avoid TagLib's high level API for rendering just copying parts of + // the file that don't contain tag data. + // + // Now I'll explain the steps in this ugliness: + + // First, make sure that we're working with a buffer that is longer than + // the *difference* in the tag sizes. We want to avoid overwriting parts + // that aren't yet in memory, so this is necessary. + TagLib::ulong bufferLength = bufferSize(); + + while (data.size() - replace > bufferLength) + bufferLength += bufferSize(); + + // Set where to start the reading and writing. + long readPosition = start + replace; + long writePosition = start; + ByteVector buffer; + ByteVector aboutToOverwrite(static_cast<TagLib::uint>(bufferLength)); + + // This is basically a special case of the loop below. Here we're just + // doing the same steps as below, but since we aren't using the same buffer + // size -- instead we're using the tag size -- this has to be handled as a + // special case. We're also using File::writeBlock() just for the tag. + // That's a bit slower than using char *'s so, we're only doing it here. + seek(readPosition); + ssize_t bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength); + if (bytesRead <= 0) + return; // error + readPosition += bufferLength; + + seek(writePosition); + writeBlock(data); + writePosition += data.size(); + + buffer = aboutToOverwrite; + buffer.resize(bytesRead); + + // Ok, here's the main loop. We want to loop until the read fails, which + // means that we hit the end of the file. + while (!buffer.isEmpty()) + { + // Seek to the current read position and read the data that we're about + // to overwrite. Appropriately increment the readPosition. + seek(readPosition); + bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength); + if (bytesRead <= 0) + return; // error + aboutToOverwrite.resize(bytesRead); + readPosition += bufferLength; + + // Check to see if we just read the last block. We need to call clear() + // if we did so that the last write succeeds. + if (TagLib::ulong(bytesRead) < bufferLength) + clear(); + + // Seek to the write position and write our buffer. Increment the + // writePosition. + seek(writePosition); + if (m_file.Write(buffer.data(), buffer.size()) < static_cast<ssize_t>(buffer.size())) + return; // error + writePosition += buffer.size(); + + buffer = aboutToOverwrite; + bufferLength = bytesRead; + } +} + +/*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ +void TagLibVFSStream::removeBlock(TagLib::ulong start, TagLib::ulong length) +{ + TagLib::ulong bufferLength = bufferSize(); + + long readPosition = start + length; + long writePosition = start; + + ByteVector buffer(static_cast<TagLib::uint>(bufferLength)); + + TagLib::ulong bytesRead = 1; + + while(bytesRead != 0) + { + seek(readPosition); + ssize_t read = m_file.Read(buffer.data(), bufferLength); + if (read < 0) + return;// explicit error + + bytesRead = static_cast<TagLib::ulong>(read); + readPosition += bytesRead; + + // Check to see if we just read the last block. We need to call clear() + // if we did so that the last write succeeds. + if(bytesRead < bufferLength) + clear(); + + seek(writePosition); + if (m_file.Write(buffer.data(), bytesRead) != static_cast<ssize_t>(bytesRead)) + return; // error + writePosition += bytesRead; + } + truncate(writePosition); +} + +/*! + * Returns true if the file is read only (or if the file can not be opened). + */ +bool TagLibVFSStream::readOnly() const +{ + return m_bIsReadOnly; +} + +/*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ +bool TagLibVFSStream::isOpen() const +{ + return m_bIsOpen; +} + +/*! + * Move the I/O pointer to \a offset in the file from position \a p. This + * defaults to seeking from the beginning of the file. + * + * \see Position + */ +void TagLibVFSStream::seek(long offset, Position p) +{ + const long fileLen = length(); + if (m_bIsReadOnly && fileLen > 0) + { + long startPos; + if (p == Beginning) + startPos = 0; + else if (p == Current) + startPos = tell(); + else if (p == End) + startPos = fileLen; + else + return; // wrong Position value + + // When parsing some broken files, taglib may try to seek above end of file. + // If underlying VFS does not move I/O pointer in this case, taglib will parse + // same part of file several times and ends with error. To prevent this + // situation, force seek to last valid position so VFS move I/O pointer. + if (startPos >= 0) + { + if (offset < 0 && startPos + offset < 0) + { + m_file.Seek(0, SEEK_SET); + return; + } + if (offset > 0 && startPos + offset > fileLen) + { + m_file.Seek(fileLen, SEEK_SET); + return; + } + } + } + + switch(p) + { + case Beginning: + m_file.Seek(offset, SEEK_SET); + break; + case Current: + m_file.Seek(offset, SEEK_CUR); + break; + case End: + m_file.Seek(offset, SEEK_END); + break; + } +} + +/*! + * Reset the end-of-file and error flags on the file. + */ +void TagLibVFSStream::clear() +{ +} + +/*! + * Returns the current offset within the file. + */ +long TagLibVFSStream::tell() const +{ + int64_t pos = m_file.GetPosition(); + if(pos > LONG_MAX) + return -1; + else + return (long)pos; +} + +/*! + * Returns the length of the file. + */ +long TagLibVFSStream::length() +{ + return (long)m_file.GetLength(); +} + +/*! + * Truncates the file to a \a length. + */ +void TagLibVFSStream::truncate(long length) +{ + m_file.Truncate(length); +} diff --git a/xbmc/music/tags/TagLibVFSStream.h b/xbmc/music/tags/TagLibVFSStream.h new file mode 100644 index 0000000..d567055 --- /dev/null +++ b/xbmc/music/tags/TagLibVFSStream.h @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/File.h" + +#include <taglib/tiostream.h> + +namespace MUSIC_INFO +{ + class TagLibVFSStream : public TagLib::IOStream + { + public: + /*! + * Construct a File object and opens the \a file. \a file should be a + * be an XBMC Vfile. + */ + TagLibVFSStream(const std::string& strFileName, bool readOnly); + + /*! + * Destroys this ByteVectorStream instance. + */ + ~TagLibVFSStream() override; + + /*! + * Returns the file name in the local file system encoding. + */ + TagLib::FileName name() const override; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + TagLib::ByteVector readBlock(TagLib::ulong length) override; + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + void writeBlock(const TagLib::ByteVector &data) override; + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + void insert(const TagLib::ByteVector &data, TagLib::ulong start = 0, TagLib::ulong replace = 0) override; + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + void removeBlock(TagLib::ulong start = 0, TagLib::ulong length = 0) override; + + /*! + * Returns true if the file is read only (or if the file can not be opened). + */ + bool readOnly() const override; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + bool isOpen() const override; + + /*! + * Move the I/O pointer to \a offset in the file from position \a p. This + * defaults to seeking from the beginning of the file. + * + * \see Position + */ + void seek(long offset, TagLib::IOStream::Position p = Beginning) override; + + /*! + * Reset the end-of-file and error flags on the file. + */ + void clear() override; + + /*! + * Returns the current offset within the file. + */ + long tell() const override; + + /*! + * Returns the length of the file. + */ + long length() override; + + /*! + * Truncates the file to a \a length. + */ + void truncate(long length) override; + + protected: + /*! + * Returns the buffer size that is used for internal buffering. + */ + static TagLib::uint bufferSize() { return 1024; } + + private: + std::string m_strFileName; + XFILE::CFile m_file; + bool m_bIsReadOnly; + bool m_bIsOpen; + }; +} + diff --git a/xbmc/music/tags/TagLoaderTagLib.cpp b/xbmc/music/tags/TagLoaderTagLib.cpp new file mode 100644 index 0000000..6eb591a --- /dev/null +++ b/xbmc/music/tags/TagLoaderTagLib.cpp @@ -0,0 +1,1371 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "TagLoaderTagLib.h" + +#include <vector> + +#include <taglib/id3v1tag.h> +#include <taglib/apetag.h> +#include <taglib/asftag.h> +#include <taglib/id3v1genres.h> +#include <taglib/aifffile.h> +#include <taglib/apefile.h> +#include <taglib/asffile.h> +#include <taglib/modfile.h> +#include <taglib/mp4file.h> +#include <taglib/mpegfile.h> +#include <taglib/oggfile.h> +#include <taglib/oggflacfile.h> +#include <taglib/opusfile.h> +#include <taglib/rifffile.h> +#include <taglib/speexfile.h> +#include <taglib/s3mfile.h> +#include <taglib/trueaudiofile.h> +#include <taglib/vorbisfile.h> +#include <taglib/wavfile.h> +#include <taglib/wavpackfile.h> +#include <taglib/xmfile.h> +#include <taglib/flacfile.h> +#include <taglib/itfile.h> +#include <taglib/mpcfile.h> +#include <taglib/id3v2tag.h> +#include <taglib/xiphcomment.h> +#include <taglib/mp4tag.h> + +#include <taglib/textidentificationframe.h> +#include <taglib/uniquefileidentifierframe.h> +#include <taglib/popularimeterframe.h> +#include <taglib/commentsframe.h> +#include <taglib/unsynchronizedlyricsframe.h> +#include <taglib/attachedpictureframe.h> + +#include <taglib/tstring.h> +#include <taglib/tpropertymap.h> + +#include "TagLibVFSStream.h" +#include "MusicInfoTag.h" +#include "ReplayGain.h" +#include "utils/RegExp.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "utils/StringUtils.h" +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" + +#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11 +#include "utils/Base64.h" +#endif + +using namespace TagLib; +using namespace MUSIC_INFO; + +namespace +{ +std::vector<std::string> StringListToVectorString(const StringList& stringList) +{ + std::vector<std::string> values; + for (const auto& it: stringList) + values.push_back(it.to8Bit(true)); + return values; +} + +std::vector<std::string> GetASFStringList(const List<ASF::Attribute>& list) +{ + std::vector<std::string> values; + for (const auto& at: list) + values.push_back(at.toString().to8Bit(true)); + return values; +} + +std::vector<std::string> GetID3v2StringList(const ID3v2::FrameList& frameList) +{ + auto frame = dynamic_cast<const ID3v2::TextIdentificationFrame *>(frameList.front()); + if (frame) + return StringListToVectorString(frame->fieldList()); + return std::vector<std::string>(); +} + +void SetFlacArt(FLAC::File *flacFile, EmbeddedArt *art, CMusicInfoTag &tag) +{ + FLAC::Picture *cover[2] = {}; + auto pictures = flacFile->pictureList(); + for (List<FLAC::Picture *>::ConstIterator i = pictures.begin(); i != pictures.end(); ++i) + { + FLAC::Picture *picture = *i; + if (picture->type() == FLAC::Picture::FrontCover) + cover[0] = picture; + else // anything else is taken as second priority + cover[1] = picture; + } + for (const FLAC::Picture* const c : cover) + { + if (c) + { + tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true)); + if (art) + art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true)); + return; // one is enough + } + } +} +} + +bool CTagLoaderTagLib::Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag, EmbeddedArt *art /* = NULL */) +{ + return Load(strFileName, tag, "", art); +} + + +template<> +bool CTagLoaderTagLib::ParseTag(ASF::Tag *asf, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!asf) + return false; + + ReplayGain replayGainInfo; + tag.SetTitle(asf->title().to8Bit(true)); + const ASF::AttributeListMap& attributeListMap = asf->attributeListMap(); + for (ASF::AttributeListMap::ConstIterator it = attributeListMap.begin(); it != attributeListMap.end(); ++it) + { + if (it->first == "Author") + SetArtist(tag, GetASFStringList(it->second)); + else if (it->first == "WM/ArtistSortOrder") + SetArtistSort(tag, GetASFStringList(it->second)); + else if (it->first == "WM/AlbumArtist") + SetAlbumArtist(tag, GetASFStringList(it->second)); + else if (it->first == "WM/AlbumArtistSortOrder") + SetAlbumArtistSort(tag, GetASFStringList(it->second)); + else if (it->first == "WM/ComposerSortOrder") + SetComposerSort(tag, GetASFStringList(it->second)); + else if (it->first == "WM/AlbumTitle") + tag.SetAlbum(it->second.front().toString().to8Bit(true)); + else if (it->first == "WM/TrackNumber" || + it->first == "WM/Track") + { + if (it->second.front().type() == ASF::Attribute::DWordType) + tag.SetTrackNumber(it->second.front().toUInt()); + else + tag.SetTrackNumber(atoi(it->second.front().toString().toCString(true))); + } + else if (it->first == "WM/PartOfSet") + tag.SetDiscNumber(atoi(it->second.front().toString().toCString(true))); + else if (it->first == "WM/Genre") + SetGenre(tag, GetASFStringList(it->second)); + else if (it->first == "WM/Mood") + tag.SetMood(it->second.front().toString().to8Bit(true)); + else if (it->first == "WM/Composer") + AddArtistRole(tag, "Composer", GetASFStringList(it->second)); + else if (it->first == "WM/Conductor") + AddArtistRole(tag, "Conductor", GetASFStringList(it->second)); + //No ASF/WMA tag from Taglib for "ensemble" + else if (it->first == "WM/Writer") + AddArtistRole(tag, "Lyricist", GetASFStringList(it->second)); + else if (it->first == "WM/ModifiedBy") + AddArtistRole(tag, "Remixer", GetASFStringList(it->second)); + else if (it->first == "WM/Engineer") + AddArtistRole(tag, "Engineer", GetASFStringList(it->second)); + else if (it->first == "WM/Producer") + AddArtistRole(tag, "Producer", GetASFStringList(it->second)); + else if (it->first == "WM/DJMixer") + AddArtistRole(tag, "DJMixer", GetASFStringList(it->second)); + else if (it->first == "WM/Mixer") + AddArtistRole(tag, "mixer", GetASFStringList(it->second)); + else if (it->first == "WM/Publisher") + tag.SetRecordLabel(it->second.front().toString().to8Bit(true)); + else if (it->first == "WM/Script") + {} // Known unsupported, suppress warnings + else if (it->first == "WM/Year") + tag.SetReleaseDate(it->second.front().toString().to8Bit(true)); + else if (it->first == "WM/OriginalReleaseYear") + tag.SetOriginalDate(it->second.front().toString().to8Bit(true)); + else if (it->first == "WM/SetSubTitle") + tag.SetDiscSubtitle(it->second.front().toString().to8Bit(true)); + else if (it->first == "MusicBrainz/Artist Id") + tag.SetMusicBrainzArtistID(SplitMBID(GetASFStringList(it->second))); + else if (it->first == "MusicBrainz/Album Id") + tag.SetMusicBrainzAlbumID(it->second.front().toString().to8Bit(true)); + else if (it->first == "MusicBrainz/Release Group Id") + tag.SetMusicBrainzReleaseGroupID(it->second.front().toString().to8Bit(true)); + else if (it->first == "MusicBrainz/Album Artist") + SetAlbumArtist(tag, GetASFStringList(it->second)); + else if (it->first == "MusicBrainz/Album Artist Id") + tag.SetMusicBrainzAlbumArtistID(SplitMBID(GetASFStringList(it->second))); + else if (it->first == "MusicBrainz/Track Id") + tag.SetMusicBrainzTrackID(it->second.front().toString().to8Bit(true)); + else if (it->first == "MusicBrainz/Album Status") + tag.SetAlbumReleaseStatus(it->second.front().toString().toCString(true)); + else if (it->first == "MusicBrainz/Album Type") + SetReleaseType(tag, GetASFStringList(it->second)); + else if (it->first == "MusicIP/PUID") + {} + else if (it->first == "WM/BeatsPerMinute") + tag.SetBPM(atoi(it->second.front().toString().toCString(true))); + else if (it->first == "replaygain_track_gain" || it->first == "REPLAYGAIN_TRACK_GAIN") + replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toString().toCString(true)); + else if (it->first == "replaygain_album_gain" || it->first == "REPLAYGAIN_ALBUM_GAIN") + replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toString().toCString(true)); + else if (it->first == "replaygain_track_peak" || it->first == "REPLAYGAIN_TRACK_PEAK") + replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toString().toCString(true)); + else if (it->first == "replaygain_album_peak" || it->first == "REPLAYGAIN_ALBUM_PEAK") + replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toString().toCString(true)); + else if (it->first == "WM/Picture") + { // picture + ASF::Picture pic = it->second.front().toPicture(); + tag.SetCoverArtInfo(pic.picture().size(), pic.mimeType().toCString()); + if (art) + art->Set(reinterpret_cast<const uint8_t *>(pic.picture().data()), pic.picture().size(), pic.mimeType().toCString()); + } + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX) + CLog::Log(LOGDEBUG, "unrecognized ASF tag name: {}", it->first.toCString(true)); + } + // artist may be specified in the ContentDescription block rather than using the 'Author' attribute. + if (tag.GetArtist().empty()) + tag.SetArtist(asf->artist().toCString(true)); + + if (!asf->comment().isEmpty()) + tag.SetComment(asf->comment().toCString(true)); + tag.SetReplayGain(replayGainInfo); + tag.SetLoaded(true); + return true; +} + +int CTagLoaderTagLib::POPMtoXBMC(int popm) +{ + // Ratings: + // FROM: http://www.mediamonkey.com/forum/viewtopic.php?f=7&t=40532&start=30#p391067 + // The following schemes are used by the other POPM-compatible players: + // WMP/Vista: "Windows Media Player 9 Series" ratings: + // 1 = 1, 2 = 64, 3=128, 4=196 (not 192), 5=255 + // MediaMonkey (v4.2.1): "no@email" ratings: + // 0.5=13, 1=1, 1.5=54, 2=64, 2.5=118, + // 3=128, 3.5=186, 4=196, 4.5=242, 5=255 + // Note 1 star written as 1 while half a star is 13, a higher value + // Accommodate these mapped values in a scale from 0-255 + if (popm == 0) return 0; + if (popm == 1) return 2; + if (popm < 23) return 1; + if (popm < 32) return 2; + if (popm < 64) return 3; + if (popm < 96) return 4; + if (popm < 128) return 5; + if (popm < 160) return 6; + if (popm < 196) return 7; + if (popm < 224) return 8; + if (popm < 255) return 9; + else return 10; +} + +template<> +bool CTagLoaderTagLib::ParseTag(ID3v1::Tag *id3v1, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!id3v1) return false; + tag.SetTitle(id3v1->title().to8Bit(true)); + tag.SetArtist(id3v1->artist().to8Bit(true)); + tag.SetAlbum(id3v1->album().to8Bit(true)); + tag.SetComment(id3v1->comment().to8Bit(true)); + tag.SetGenre(id3v1->genre().to8Bit(true), true); + tag.SetYear(id3v1->year()); + tag.SetTrackNumber(id3v1->track()); + return true; +} + +template<> +bool CTagLoaderTagLib::ParseTag(ID3v2::Tag *id3v2, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& tag) +{ + if (!id3v2) return false; + ReplayGain replayGainInfo; + + ID3v2::AttachedPictureFrame *pictures[3] = {}; + const ID3v2::FrameListMap& frameListMap = id3v2->frameListMap(); + for (ID3v2::FrameListMap::ConstIterator it = frameListMap.begin(); it != frameListMap.end(); ++it) + { + // It is possible that the taglist is empty. In that case no useable values can be extracted. + // and we should skip the tag. + if (it->second.isEmpty()) continue; + + if (it->first == "TPE1") SetArtist(tag, GetID3v2StringList(it->second)); + else if (it->first == "TSOP") SetArtistSort(tag, GetID3v2StringList(it->second)); + else if (it->first == "TALB") tag.SetAlbum(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TPE2") SetAlbumArtist(tag, GetID3v2StringList(it->second)); + else if (it->first == "TSO2") SetAlbumArtistSort(tag, GetID3v2StringList(it->second)); + else if (it->first == "TSOC") SetComposerSort(tag, GetID3v2StringList(it->second)); + else if (it->first == "TIT2") tag.SetTitle(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TCON") SetGenre(tag, GetID3v2StringList(it->second)); + else if (it->first == "TRCK") + tag.SetTrackNumber( + static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10))); + else if (it->first == "TPOS") + tag.SetDiscNumber( + static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10))); + else if (it->first == "TDOR" || it->first == "TORY") // TDOR - ID3v2.4, TORY - ID3v2.3 + tag.SetOriginalDate(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TDAT") {} // empty as taglib has moved the value to TDRC + else if (it->first == "TCMP") tag.SetCompilation((strtol(it->second.front()->toString().toCString(true), nullptr, 10) == 0) ? false : true); + else if (it->first == "TENC") {} // EncodedBy + else if (it->first == "TCOM") AddArtistRole(tag, "Composer", GetID3v2StringList(it->second)); + else if (it->first == "TPE3") AddArtistRole(tag, "Conductor", GetID3v2StringList(it->second)); + else if (it->first == "TEXT") AddArtistRole(tag, "Lyricist", GetID3v2StringList(it->second)); + else if (it->first == "TPE4") AddArtistRole(tag, "Remixer", GetID3v2StringList(it->second)); + else if (it->first == "TPUB") tag.SetRecordLabel(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TCOP") {} // Copyright message + else if (it->first == "TDRC") // taglib concatenates TYER & TDAT into this field if v2.3 + tag.SetReleaseDate(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TDRL") {} // Not set by Picard or used in community generally + else if (it->first == "TDTG") {} // Tagging time + else if (it->first == "TLAN") {} // Languages + else if (it->first == "TMOO") tag.SetMood(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TSST") + tag.SetDiscSubtitle(it->second.front()->toString().to8Bit(true)); + else if (it->first == "TBPM") + tag.SetBPM( + static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10))); + else if (it->first == "USLT") + // Loop through any lyrics frames. Could there be multiple frames, how to choose? + for (ID3v2::FrameList::ConstIterator lt = it->second.begin(); lt != it->second.end(); ++lt) + { + auto lyricsFrame = dynamic_cast<ID3v2::UnsynchronizedLyricsFrame *> (*lt); + if (lyricsFrame) + tag.SetLyrics(lyricsFrame->text().to8Bit(true)); + } + else if (it->first == "COMM") + // Loop through and look for the main (no description) comment + for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct) + { + ID3v2::CommentsFrame *commentsFrame = dynamic_cast<ID3v2::CommentsFrame *> (*ct); + if (commentsFrame && commentsFrame->description().isEmpty()) + tag.SetComment(commentsFrame->text().to8Bit(true)); + } + else if (it->first == "TXXX") + // Loop through and process the UserTextIdentificationFrames + for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut) + { + ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<ID3v2::UserTextIdentificationFrame *> (*ut); + if (!frame) continue; + + // First field is the same as the description + StringList stringList = frame->fieldList(); + if (stringList.size() == 1) continue; + stringList.erase(stringList.begin()); + String desc = frame->description().upper(); + if (desc == "MUSICBRAINZ ARTIST ID") + tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(stringList))); + else if (desc == "MUSICBRAINZ ALBUM ID") + tag.SetMusicBrainzAlbumID(stringList.front().to8Bit(true)); + else if (desc == "MUSICBRAINZ RELEASE GROUP ID") + tag.SetMusicBrainzReleaseGroupID(stringList.front().to8Bit(true)); + else if (desc == "MUSICBRAINZ ALBUM ARTIST ID") + tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(stringList))); + else if (desc == "MUSICBRAINZ ALBUM ARTIST") + SetAlbumArtist(tag, StringListToVectorString(stringList)); + else if (desc == "MUSICBRAINZ ALBUM TYPE") + SetReleaseType(tag, StringListToVectorString(stringList)); + else if (desc == "MUSICBRAINZ ALBUM STATUS") + tag.SetAlbumReleaseStatus(stringList.front().to8Bit(true)); + else if (desc == "REPLAYGAIN_TRACK_GAIN") + replayGainInfo.ParseGain(ReplayGain::TRACK, stringList.front().toCString(true)); + else if (desc == "REPLAYGAIN_ALBUM_GAIN") + replayGainInfo.ParseGain(ReplayGain::ALBUM, stringList.front().toCString(true)); + else if (desc == "REPLAYGAIN_TRACK_PEAK") + replayGainInfo.ParsePeak(ReplayGain::TRACK, stringList.front().toCString(true)); + else if (desc == "REPLAYGAIN_ALBUM_PEAK") + replayGainInfo.ParsePeak(ReplayGain::ALBUM, stringList.front().toCString(true)); + else if (desc == "ALBUMARTIST" || desc == "ALBUM ARTIST") + SetAlbumArtist(tag, StringListToVectorString(stringList)); + else if (desc == "ALBUMARTISTSORT" || desc == "ALBUM ARTIST SORT") + SetAlbumArtistSort(tag, StringListToVectorString(stringList)); + else if (desc == "ARTISTS") + SetArtistHints(tag, StringListToVectorString(stringList)); + else if (desc == "ALBUMARTISTS" || desc == "ALBUM ARTISTS") + SetAlbumArtistHints(tag, StringListToVectorString(stringList)); + else if (desc == "WRITER") // How Picard >1.3 tags writer in ID3 + AddArtistRole(tag, "Writer", StringListToVectorString(stringList)); + else if (desc == "COMPOSERSORT" || desc == "COMPOSER SORT") + SetComposerSort(tag, StringListToVectorString(stringList)); + else if (desc == "MOOD") + tag.SetMood(stringList.front().to8Bit(true)); + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX) + CLog::Log(LOGDEBUG, "unrecognized user text tag detected: TXXX:{}", + frame->description().toCString(true)); + } + else if (it->first == "TIPL") + // Loop through and process the involved people list + // For example Arranger, Engineer, Producer, DJMixer or Mixer + // In fieldlist every odd field is a function, and every even is an artist or a comma delimited list of artists. + for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip) + { + auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip); + if (tiplframe) + AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList())); + } + else if (it->first == "TMCL") + // Loop through and process the musician credits list + // It is a mapping between the instrument and the person that played it, but also includes "orchestra" or "soloist". + // In fieldlist every odd field is an instrument, and every even is an artist or a comma delimited list of artists. + for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip) + { + auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip); + if (tiplframe) + AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList())); + } + else if (it->first == "UFID") + // Loop through any UFID frames and set them + for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut) + { + auto ufid = dynamic_cast<ID3v2::UniqueFileIdentifierFrame*> (*ut); + if (ufid && ufid->owner() == "http://musicbrainz.org") + { + // MusicBrainz pads with a \0, but the spec requires binary, be cautious + char cUfid[64]; + int max_size = std::min(static_cast<int>(ufid->identifier().size()), 63); + strncpy(cUfid, ufid->identifier().data(), max_size); + cUfid[max_size] = '\0'; + tag.SetMusicBrainzTrackID(cUfid); + } + } + else if (it->first == "APIC") + // Loop through all pictures and store the frame pointers for the picture types we want + for (ID3v2::FrameList::ConstIterator pi = it->second.begin(); pi != it->second.end(); ++pi) + { + auto pictureFrame = dynamic_cast<ID3v2::AttachedPictureFrame *> (*pi); + if (!pictureFrame) continue; + + if (pictureFrame->type() == ID3v2::AttachedPictureFrame::FrontCover) pictures[0] = pictureFrame; + else if (pictureFrame->type() == ID3v2::AttachedPictureFrame::Other) pictures[1] = pictureFrame; + else if (pi == it->second.begin()) pictures[2] = pictureFrame; + } + else if (it->first == "POPM") + // Loop through and process ratings + for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct) + { + auto popFrame = dynamic_cast<ID3v2::PopularimeterFrame *> (*ct); + if (!popFrame) continue; + + // @xbmc.org ratings trump others (of course) + if (popFrame->email() == "ratings@xbmc.org") + tag.SetUserrating(popFrame->rating() / 51); //! @todo wtf? Why 51 find some explanation, somewhere... + else if (tag.GetUserrating() == 0) + { + if (popFrame->email() != "Windows Media Player 9 Series" && + popFrame->email() != "Banshee" && + popFrame->email() != "no@email" && + popFrame->email() != "quodlibet@lists.sacredchao.net" && + popFrame->email() != "rating@winamp.com") + CLog::Log(LOGDEBUG, "unrecognized ratings schema detected: {}", + popFrame->email().toCString(true)); + tag.SetUserrating(POPMtoXBMC(popFrame->rating())); + } + } + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX) + CLog::Log(LOGDEBUG, "unrecognized ID3 frame detected: {}{}{}{}", it->first[0], it->first[1], + it->first[2], it->first[3]); + } // for + + // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = First Found picture + for (const ID3v2::AttachedPictureFrame* const picture : pictures) + if (picture) + { + std::string mime = picture->mimeType().to8Bit(true); + TagLib::uint size = picture->picture().size(); + tag.SetCoverArtInfo(size, mime); + if (art) + art->Set(reinterpret_cast<const uint8_t*>(picture->picture().data()), size, mime); + + // Stop after we find the first picture for now. + break; + } + + + if (!id3v2->comment().isEmpty()) + tag.SetComment(id3v2->comment().toCString(true)); + + tag.SetReplayGain(replayGainInfo); + return true; +} + +template<> +bool CTagLoaderTagLib::ParseTag(APE::Tag *ape, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!ape) + return false; + + ReplayGain replayGainInfo; + const APE::ItemListMap itemListMap = ape->itemListMap(); + for (APE::ItemListMap::ConstIterator it = itemListMap.begin(); it != itemListMap.end(); ++it) + { + if (it->first == "ARTIST") + SetArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ARTISTSORT") + SetArtistSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ARTISTS") + SetArtistHints(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST") + SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ALBUMARTISTSORT") + SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS") + SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "COMPOSERSORT") + SetComposerSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "ALBUM") + tag.SetAlbum(it->second.toString().to8Bit(true)); + else if (it->first == "TITLE") + tag.SetTitle(it->second.toString().to8Bit(true)); + else if (it->first == "TRACKNUMBER" || it->first == "TRACK") + tag.SetTrackNumber(it->second.toString().toInt()); + else if (it->first == "DISCNUMBER" || it->first == "DISC") + tag.SetDiscNumber(it->second.toString().toInt()); + else if (it->first == "YEAR") + tag.SetReleaseDate(it->second.toString().to8Bit(true)); + else if (it->first == "DISCSUBTITLE") + tag.SetDiscSubtitle(it->second.toString().to8Bit(true)); + else if (it->first == "ORIGINALYEAR") + tag.SetOriginalDate(it->second.toString().to8Bit(true)); + else if (it->first == "GENRE") + SetGenre(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "MOOD") + tag.SetMood(it->second.toString().to8Bit(true)); + else if (it->first == "COMMENT") + tag.SetComment(it->second.toString().to8Bit(true)); + else if (it->first == "CUESHEET") + tag.SetCueSheet(it->second.toString().to8Bit(true)); + else if (it->first == "ENCODEDBY") + {} + else if (it->first == "COMPOSER") + AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "CONDUCTOR") + AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList())); + else if (it->first == "BAND") + AddArtistRole(tag, "Band", StringListToVectorString(it->second.toStringList())); + else if (it->first == "ENSEMBLE") + AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second.toStringList())); + else if (it->first == "LYRICIST") + AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList())); + else if (it->first == "WRITER") + AddArtistRole(tag, "Writer", StringListToVectorString(it->second.toStringList())); + else if ((it->first == "MIXARTIST") || (it->first == "REMIXER")) + AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "ARRANGER") + AddArtistRole(tag, "Arranger", StringListToVectorString(it->second.toStringList())); + else if (it->first == "ENGINEER") + AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "PRODUCER") + AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "DJMIXER") + AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "MIXER") + AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "PERFORMER") + // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)" + AddArtistInstrument(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "LABEL") + tag.SetRecordLabel(it->second.toString().to8Bit(true)); + else if (it->first == "COMPILATION") + tag.SetCompilation(it->second.toString().toInt() == 1); + else if (it->first == "LYRICS") + tag.SetLyrics(it->second.toString().to8Bit(true)); + else if (it->first == "REPLAYGAIN_TRACK_GAIN") + replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toString().toCString(true)); + else if (it->first == "REPLAYGAIN_ALBUM_GAIN") + replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toString().toCString(true)); + else if (it->first == "REPLAYGAIN_TRACK_PEAK") + replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toString().toCString(true)); + else if (it->first == "REPLAYGAIN_ALBUM_PEAK") + replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toString().toCString(true)); + else if (it->first == "MUSICBRAINZ_ARTISTID") + tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList()))); + else if (it->first == "MUSICBRAINZ_ALBUMARTISTID") + tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList()))); + else if (it->first == "MUSICBRAINZ_ALBUMARTIST") + SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "MUSICBRAINZ_ALBUMID") + tag.SetMusicBrainzAlbumID(it->second.toString().to8Bit(true)); + else if (it->first == "MUSICBRAINZ_RELEASEGROUPID") + tag.SetMusicBrainzReleaseGroupID(it->second.toString().to8Bit(true)); + else if (it->first == "MUSICBRAINZ_TRACKID") + tag.SetMusicBrainzTrackID(it->second.toString().to8Bit(true)); + else if (it->first == "MUSICBRAINZ_ALBUMTYPE") + SetReleaseType(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "BPM") + tag.SetBPM(it->second.toString().toInt()); + else if (it->first == "MUSICBRAINZ_ALBUMSTATUS") + tag.SetAlbumReleaseStatus(it->second.toString().to8Bit(true)); + else if (it->first == "COVER ART (FRONT)") + { + TagLib::ByteVector tdata = it->second.binaryData(); + // The image data follows a null byte, which can optionally be preceded by a filename + const uint offset = tdata.find('\0') + 1; + ByteVector bv(tdata.data() + offset, tdata.size() - offset); + // Infer the mimetype + std::string mime{}; + if (bv.startsWith("\xFF\xD8\xFF")) + mime = "image/jpeg"; + else if (bv.startsWith("\x89\x50\x4E\x47")) + mime = "image/png"; + else if (bv.startsWith("\x47\x49\x46\x38")) + mime = "image/gif"; + else if (bv.startsWith("\x42\x4D")) + mime = "image/bmp"; + if ((offset > 0) && (offset <= tdata.size()) && (mime.size() > 0)) + { + tag.SetCoverArtInfo(bv.size(), mime); + if (art) + art->Set(reinterpret_cast<const uint8_t*>(bv.data()), bv.size(), mime); + } + } + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX) + CLog::Log(LOGDEBUG, "unrecognized APE tag: {}", it->first.toCString(true)); + } + + tag.SetReplayGain(replayGainInfo); + return true; +} + +template<> +bool CTagLoaderTagLib::ParseTag(Ogg::XiphComment *xiph, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!xiph) + return false; + +#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11 + FLAC::Picture pictures[3]; +#endif + ReplayGain replayGainInfo; + + const Ogg::FieldListMap& fieldListMap = xiph->fieldListMap(); + for (Ogg::FieldListMap::ConstIterator it = fieldListMap.begin(); it != fieldListMap.end(); ++it) + { + if (it->first == "ARTIST") + SetArtist(tag, StringListToVectorString(it->second)); + else if (it->first == "ARTISTSORT") + SetArtistSort(tag, StringListToVectorString(it->second)); + else if (it->first == "ARTISTS") + SetArtistHints(tag, StringListToVectorString(it->second)); + else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST") + SetAlbumArtist(tag, StringListToVectorString(it->second)); + else if (it->first == "ALBUMARTISTSORT" || it->first == "ALBUM ARTIST SORT") + SetAlbumArtistSort(tag, StringListToVectorString(it->second)); + else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS") + SetAlbumArtistHints(tag, StringListToVectorString(it->second)); + else if (it->first == "COMPOSERSORT") + SetComposerSort(tag, StringListToVectorString(it->second)); + else if (it->first == "ALBUM") + tag.SetAlbum(it->second.front().to8Bit(true)); + else if (it->first == "TITLE") + tag.SetTitle(it->second.front().to8Bit(true)); + else if (it->first == "TRACKNUMBER") + tag.SetTrackNumber(it->second.front().toInt()); + else if (it->first == "DISCNUMBER") + tag.SetDiscNumber(it->second.front().toInt()); + else if (it->first == "YEAR" || it->first == "DATE") + tag.AddReleaseDate(it->second.front().to8Bit(true)); + else if (it->first == "GENRE") + SetGenre(tag, StringListToVectorString(it->second)); + else if (it->first == "MOOD") + tag.SetMood(it->second.front().to8Bit(true)); + else if (it->first == "COMMENT") + tag.SetComment(it->second.front().to8Bit(true)); + else if (it->first == "ORIGINALYEAR" || it->first == "ORIGINALDATE") + tag.AddOriginalDate(it->second.front().to8Bit(true)); + else if (it->first == "CUESHEET") + tag.SetCueSheet(it->second.front().to8Bit(true)); + else if (it->first == "DISCSUBTITLE") + tag.SetDiscSubtitle(it->second.front().to8Bit(true)); + else if (it->first == "ENCODEDBY") + {} // Known but unsupported, suppress warnings + else if (it->first == "COMPOSER") + AddArtistRole(tag, "Composer", StringListToVectorString(it->second)); + else if (it->first == "CONDUCTOR") + AddArtistRole(tag, "Conductor", StringListToVectorString(it->second)); + else if (it->first == "BAND") + AddArtistRole(tag, "Band", StringListToVectorString(it->second)); + else if (it->first == "ENSEMBLE") + AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second)); + else if (it->first == "LYRICIST") + AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second)); + else if (it->first == "WRITER") + AddArtistRole(tag, "Writer", StringListToVectorString(it->second)); + else if ((it->first == "MIXARTIST") || (it->first == "REMIXER")) + AddArtistRole(tag, "Remixer", StringListToVectorString(it->second)); + else if (it->first == "ARRANGER") + AddArtistRole(tag, "Arranger", StringListToVectorString(it->second)); + else if (it->first == "ENGINEER") + AddArtistRole(tag, "Engineer", StringListToVectorString(it->second)); + else if (it->first == "PRODUCER") + AddArtistRole(tag, "Producer", StringListToVectorString(it->second)); + else if (it->first == "DJMIXER") + AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second)); + else if (it->first == "MIXER") + AddArtistRole(tag, "Mixer", StringListToVectorString(it->second)); + else if (it->first == "PERFORMER") + // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)" + AddArtistInstrument(tag, StringListToVectorString(it->second)); + else if (it->first == "LABEL") + tag.SetRecordLabel(it->second.front().to8Bit(true)); + else if (it->first == "COMPILATION") + tag.SetCompilation(it->second.front().toInt() == 1); + else if (it->first == "LYRICS") + tag.SetLyrics(it->second.front().to8Bit(true)); + else if (it->first == "REPLAYGAIN_TRACK_GAIN") + replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toCString(true)); + else if (it->first == "REPLAYGAIN_ALBUM_GAIN") + replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toCString(true)); + else if (it->first == "REPLAYGAIN_TRACK_PEAK") + replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toCString(true)); + else if (it->first == "REPLAYGAIN_ALBUM_PEAK") + replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toCString(true)); + else if (it->first == "MUSICBRAINZ_ARTISTID") + tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second))); + else if (it->first == "MUSICBRAINZ_ALBUMARTISTID") + tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second))); + else if (it->first == "MUSICBRAINZ_ALBUMARTIST") + SetAlbumArtist(tag, StringListToVectorString(it->second)); + else if (it->first == "MUSICBRAINZ_ALBUMID") + tag.SetMusicBrainzAlbumID(it->second.front().to8Bit(true)); + else if (it->first == "MUSICBRAINZ_RELEASEGROUPID") + tag.SetMusicBrainzReleaseGroupID(it->second.front().to8Bit(true)); + else if (it->first == "MUSICBRAINZ_TRACKID") + tag.SetMusicBrainzTrackID(it->second.front().to8Bit(true)); + else if (it->first == "RELEASETYPE") + SetReleaseType(tag, StringListToVectorString(it->second)); + else if (it->first == "BPM") + tag.SetBPM(strtol(it->second.front().toCString(true), nullptr, 10)); + else if (it->first == "RELEASESTATUS") + tag.SetAlbumReleaseStatus(it->second.front().toCString(true)); + else if (it->first == "RATING") + { + // Vorbis ratings are a mess because the standard forgot to mention anything about them. + // If you want to see how emotive the issue is and the varying standards, check here: + // http://forums.winamp.com/showthread.php?t=324512 + // The most common standard in that thread seems to be a 0-100 scale for 1-5 stars. + // So, that's what we'll support for now. + int iUserrating = it->second.front().toInt(); + if (iUserrating > 0 && iUserrating <= 100) + tag.SetUserrating((iUserrating / 10)); + } +#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11 + else if (it->first == "METADATA_BLOCK_PICTURE") + { + const char* b64 = it->second.front().toCString(); + std::string decoded_block = Base64::Decode(b64, it->second.front().size()); + ByteVector bv(decoded_block.data(), decoded_block.size()); + TagLib::FLAC::Picture* pictureFrame = new TagLib::FLAC::Picture(bv); + + if (pictureFrame->type() == FLAC::Picture::FrontCover) pictures[0].parse(bv); + else if (pictureFrame->type() == FLAC::Picture::Other) pictures[1].parse(bv); + + delete pictureFrame; + } + else if (it->first == "COVERART") + { + const char* b64 = it->second.front().toCString(); + std::string decoded_block = Base64::Decode(b64, it->second.front().size()); + ByteVector bv(decoded_block.data(), decoded_block.size()); + pictures[2].setData(bv); + // Assume jpeg + if (pictures[2].mimeType().isEmpty()) + pictures[2].setMimeType("image/jpeg"); + } + else if (it->first == "COVERARTMIME") + { + pictures[2].setMimeType(it->second.front()); + } +#endif + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX) + CLog::Log(LOGDEBUG, "unrecognized XipComment name: {}", it->first.toCString(true)); + } + +#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11 + // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = COVERART/COVERARTMIME + for (int i = 0; i < 3; ++i) + if (pictures[i].data().size()) + { + std::string mime = pictures[i].mimeType().toCString(); + if (mime.compare(0, 6, "image/") != 0) + continue; + TagLib::uint size = pictures[i].data().size(); + tag.SetCoverArtInfo(size, mime); + if (art) + art->Set(reinterpret_cast<const uint8_t*>(pictures[i].data().data()), size, mime); + + break; + } +#else + auto pictureList = xiph->pictureList(); + FLAC::Picture *cover[2] = {}; + + for (auto i: pictureList) + { + FLAC::Picture *picture = i; + if (picture->type() == FLAC::Picture::FrontCover) + cover[0] = picture; + else // anything else is taken as second priority + cover[1] = picture; + } + for (const FLAC::Picture* const c : cover) + { + if (c) + { + tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true)); + if (art) + art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true)); + break; // one is enough + } + } +#endif + + if (!xiph->comment().isEmpty()) + tag.SetComment(xiph->comment().toCString(true)); + + tag.SetReplayGain(replayGainInfo); + return true; +} + +template<> +bool CTagLoaderTagLib::ParseTag(MP4::Tag *mp4, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!mp4) + return false; + + ReplayGain replayGainInfo; + const MP4::ItemMap itemMap = mp4->itemMap(); + for (auto it = itemMap.begin(); it != itemMap.end(); ++it) + { + if (it->first == "\251nam") + tag.SetTitle(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "\251ART") + SetArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "soar") + SetArtistSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:ARTISTS") + SetArtistHints(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "\251alb") + tag.SetAlbum(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "aART") + SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "soaa") + SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:albumartists" || + it->first == "----:com.apple.iTunes:ALBUMARTISTS") + SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "soco") + SetComposerSort(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "\251gen") + SetGenre(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:MOOD") + tag.SetMood(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "\251cmt") + tag.SetComment(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "\251wrt") + AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:CONDUCTOR") + AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList())); + //No MP4 standard tag for "ensemble" + else if (it->first == "----:com.apple.iTunes:LYRICIST") + AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:REMIXER") + AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:ENGINEER") + AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:PRODUCER") + AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:DJMIXER") + AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:MIXER") + AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList())); + //No MP4 standard tag for musician credits + else if (it->first == "----:com.apple.iTunes:LABEL") + tag.SetRecordLabel(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:DISCSUBTITLE") + tag.SetDiscSubtitle(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "cpil") + tag.SetCompilation(it->second.toBool()); + else if (it->first == "trkn") + tag.SetTrackNumber(it->second.toIntPair().first); + else if (it->first == "disk") + tag.SetDiscNumber(it->second.toIntPair().first); + else if (it->first == "\251day") + tag.SetReleaseDate(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:originaldate") + tag.SetOriginalDate(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:replaygain_track_gain" || + it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN") + replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toStringList().front().toCString()); + else if (it->first == "----:com.apple.iTunes:replaygain_album_gain" || + it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN") + replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toStringList().front().toCString()); + else if (it->first == "----:com.apple.iTunes:replaygain_track_peak" || + it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK") + replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toStringList().front().toCString()); + else if (it->first == "----:com.apple.iTunes:replaygain_album_peak" || + it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK") + replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toStringList().front().toCString()); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Artist Id") + tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList()))); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist Id") + tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList()))); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist") + SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Id") + tag.SetMusicBrainzAlbumID(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Release Group Id") + tag.SetMusicBrainzReleaseGroupID(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Track Id") + tag.SetMusicBrainzTrackID(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Type") + SetReleaseType(tag, StringListToVectorString(it->second.toStringList())); + else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Status") + tag.SetAlbumReleaseStatus(it->second.toStringList().front().to8Bit(true)); + else if (it->first == "tmpo") + tag.SetBPM(it->second.toIntPair().first); + else if (it->first == "covr") + { + MP4::CoverArtList coverArtList = it->second.toCoverArtList(); + for (MP4::CoverArtList::ConstIterator pt = coverArtList.begin(); pt != coverArtList.end(); ++pt) + { + std::string mime; + switch (pt->format()) + { + case MP4::CoverArt::PNG: + mime = "image/png"; + break; + case MP4::CoverArt::JPEG: + mime = "image/jpeg"; + break; + default: + break; + } + if (mime.empty()) + continue; + tag.SetCoverArtInfo(pt->data().size(), mime); + if (art) + art->Set(reinterpret_cast<const uint8_t *>(pt->data().data()), pt->data().size(), mime); + break; // one is enough + } + } + } + + if (!mp4->comment().isEmpty()) + tag.SetComment(mp4->comment().toCString(true)); + + tag.SetReplayGain(replayGainInfo); + return true; +} + +template<> +bool CTagLoaderTagLib::ParseTag(Tag *genericTag, EmbeddedArt *art, CMusicInfoTag& tag) +{ + if (!genericTag) + return false; + + PropertyMap properties = genericTag->properties(); + for (PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it) + { + if (it->first == "ARTIST") + SetArtist(tag, StringListToVectorString(it->second)); + else if (it->first == "ALBUM") + tag.SetAlbum(it->second.front().to8Bit(true)); + else if (it->first == "TITLE") + tag.SetTitle(it->second.front().to8Bit(true)); + else if (it->first == "TRACKNUMBER") + tag.SetTrackNumber(it->second.front().toInt()); + else if (it->first == "YEAR") + tag.SetYear(it->second.front().toInt()); + else if (it->first == "GENRE") + SetGenre(tag, StringListToVectorString(it->second)); + else if (it->first == "COMMENT") + tag.SetComment(it->second.front().to8Bit(true)); + } + + return true; +} + + + + +void CTagLoaderTagLib::SetArtist(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.SetArtist(values[0]); + else + // Fill both artist vector and artist desc from tag value. + // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE + tag.SetArtist(values, true); +} + +void CTagLoaderTagLib::SetArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + // ARTISTSORT/TSOP tag is often a single string, when not take union of values + if (values.size() == 1) + tag.SetArtistSort(values[0]); + else + tag.SetArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CTagLoaderTagLib::SetArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.SetMusicBrainzArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + else + tag.SetMusicBrainzArtistHints(values); +} + +std::vector<std::string> CTagLoaderTagLib::SplitMBID(const std::vector<std::string> &values) +{ + if (values.empty() || values.size() > 1) + return values; + + // Picard, and other taggers use a heap of different separators. We use a regexp to detect + // MBIDs to make sure we hit them all... + std::vector<std::string> ret; + std::string value = values[0]; + StringUtils::ToLower(value); + CRegExp reg; + if (reg.RegComp("([[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})")) + { + int pos = -1; + while ((pos = reg.RegFind(value, pos+1)) > -1) + ret.push_back(reg.GetMatch(1)); + } + return ret; +} + +void CTagLoaderTagLib::SetAlbumArtist(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.SetAlbumArtist(values[0]); + else + // Fill both artist vector and artist desc from tag value. + // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE + tag.SetAlbumArtist(values, true); +} + +void CTagLoaderTagLib::SetAlbumArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + // ALBUMARTISTSORT/TSOP tag is often a single string, when not take union of values + if (values.size() == 1) + tag.SetAlbumArtistSort(values[0]); + else + tag.SetAlbumArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CTagLoaderTagLib::SetAlbumArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.SetMusicBrainzAlbumArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); + else + tag.SetMusicBrainzAlbumArtistHints(values); +} + +void CTagLoaderTagLib::SetComposerSort(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + // COMPOSRSORT/TSOC tag is often a single string, when not take union of values + if (values.size() == 1) + tag.SetComposerSort(values[0]); + else + tag.SetComposerSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CTagLoaderTagLib::SetGenre(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + /* + TagLib doesn't resolve ID3v1 genre numbers in the case were only + a number is specified, thus this workaround. + */ + std::vector<std::string> genres; + for (const std::string& i : values) + { + std::string genre = i; + if (StringUtils::IsNaturalNumber(genre)) + { + int number = strtol(i.c_str(), nullptr, 10); + if (number >= 0 && number < 256) + genre = ID3v1::genre(number).to8Bit(true); + } + genres.push_back(genre); + } + if (genres.size() == 1) + tag.SetGenre(genres[0], true); + else + tag.SetGenre(genres, true); +} + +void CTagLoaderTagLib::SetReleaseType(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.SetMusicBrainzReleaseType(values[0]); + else + tag.SetMusicBrainzReleaseType(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)); +} + +void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values) +{ + if (values.size() == 1) + tag.AddArtistRole(strRole, values[0]); + else + tag.AddArtistRole(strRole, values); +} + +void CTagLoaderTagLib::SetDiscSubtitle(CMusicInfoTag& tag, const std::vector<std::string>& values) +{ + if (values.size() == 1) + tag.SetDiscSubtitle(values[0]); + else + tag.SetDiscSubtitle(std::string()); +} + +void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + // Values contains role, name pairs (as in ID3 standard for TIPL or TMCL tags) + // Every odd entry is a function (e.g. Producer, Arranger etc.) or instrument (e.g. Orchestra, Vocal, Piano) + // and every even is an artist or a comma delimited list of artists. + + if (values.size() % 2 != 0) // Must contain an even number of entries + return; + + // Vector of possible separators + const std::vector<std::string> separators{ ";", "/", ",", "&", " and " }; + + for (size_t i = 0; i + 1 < values.size(); i += 2) + { + std::vector<std::string> roles; + //Split into individual roles + roles = StringUtils::Split(values[i], separators); + for (auto role : roles) + { + StringUtils::Trim(role); + StringUtils::ToCapitalize(role); + tag.AddArtistRole(role, StringUtils::Split(values[i + 1], ",")); + } + } +} + +void CTagLoaderTagLib::AddArtistInstrument(CMusicInfoTag &tag, const std::vector<std::string> &values) +{ + /* Values is a musician credits list, each entry is artist name followed by instrument (or function) + e.g. violin, drums, background vocals, solo, orchestra etc. in brackets. This is how Picard uses + the PERFORMER tag. Multiple instruments may be in one tag + e.g "Pierre Marchand (bass, drum machine and hammond organ)", + these will be separated into individual roles. + If there is not a pair of brackets then role is "performer" by default, and the whole entry is + taken as artist name. + */ + // Vector of possible separators + const std::vector<std::string> separators{";", "/", ",", "&", " and "}; + + for (size_t i = 0; i < values.size(); ++i) + { + std::vector<std::string> roles; + std::string strArtist = values[i]; + size_t firstLim = values[i].find_first_of('('); + size_t lastLim = values[i].find_last_of(')'); + if (lastLim != std::string::npos && firstLim != std::string::npos && firstLim < lastLim - 1) + { + //Pair of brackets with something between them + strArtist.erase(firstLim, lastLim - firstLim + 1); + std::string strRole = values[i].substr(firstLim + 1, lastLim - firstLim - 1); + //Split into individual roles + roles = StringUtils::Split(strRole, separators); + } + StringUtils::Trim(strArtist); + if (roles.empty()) + tag.AddArtistRole("Performer", strArtist); + else + for (auto role : roles) + { + StringUtils::Trim(role); + StringUtils::ToCapitalize(role); + tag.AddArtistRole(role, strArtist); + } + } +} + +bool CTagLoaderTagLib::Load(const std::string& strFileName, CMusicInfoTag& tag, const std::string& fallbackFileExtension, EmbeddedArt *art /* = NULL */) +{ + std::string strExtension = URIUtils::GetExtension(strFileName); + StringUtils::TrimLeft(strExtension, "."); + + if (strExtension.empty()) + { + strExtension = fallbackFileExtension; + if (strExtension.empty()) + return false; + } + + StringUtils::ToLower(strExtension); + TagLibVFSStream* stream = new TagLibVFSStream(strFileName, true); + if (!stream) + { + CLog::Log(LOGERROR, "could not create TagLib VFS stream for: {}", strFileName); + return false; + } + + long file_length = stream->length(); + + if (file_length == 0) // a stream returns zero as the length + { + delete stream; // scrap this instance + return false; // and quit without attempting to read non-existent tags + } + + TagLib::File* file = nullptr; + TagLib::APE::File* apeFile = nullptr; + TagLib::ASF::File* asfFile = nullptr; + TagLib::FLAC::File* flacFile = nullptr; + TagLib::MP4::File* mp4File = nullptr; + TagLib::MPC::File* mpcFile = nullptr; + TagLib::MPEG::File* mpegFile = nullptr; + TagLib::Ogg::Vorbis::File* oggVorbisFile = nullptr; + TagLib::Ogg::FLAC::File* oggFlacFile = nullptr; + TagLib::Ogg::Opus::File* oggOpusFile = nullptr; + TagLib::TrueAudio::File* ttaFile = nullptr; + TagLib::WavPack::File* wvFile = nullptr; + TagLib::RIFF::WAV::File * wavFile = nullptr; + TagLib::RIFF::AIFF::File * aiffFile = nullptr; + + try + { + if (strExtension == "ape") + file = apeFile = new APE::File(stream); + else if (strExtension == "asf" || strExtension == "wmv" || strExtension == "wma") + file = asfFile = new ASF::File(stream); + else if (strExtension == "flac") + file = flacFile = new FLAC::File(stream, ID3v2::FrameFactory::instance()); + else if (strExtension == "it") + file = new IT::File(stream); + else if (strExtension == "mod" || strExtension == "module" || strExtension == "nst" || strExtension == "wow") + file = new Mod::File(stream); + else if (strExtension == "mp4" || strExtension == "m4a" || strExtension == "m4v" || + strExtension == "m4r" || strExtension == "m4b" || + strExtension == "m4p" || strExtension == "3g2") + file = mp4File = new MP4::File(stream); + else if (strExtension == "mpc") + file = mpcFile = new MPC::File(stream); + else if (strExtension == "mp3" || strExtension == "aac") + file = mpegFile = new MPEG::File(stream, ID3v2::FrameFactory::instance()); + else if (strExtension == "s3m") + file = new S3M::File(stream); + else if (strExtension == "tta") + file = ttaFile = new TrueAudio::File(stream, ID3v2::FrameFactory::instance()); + else if (strExtension == "wv") + file = wvFile = new WavPack::File(stream); + else if (strExtension == "aif" || strExtension == "aiff") + file = aiffFile = new RIFF::AIFF::File(stream); + else if (strExtension == "wav") + file = wavFile = new RIFF::WAV::File(stream); + else if (strExtension == "xm") + file = new XM::File(stream); + else if (strExtension == "ogg") + file = oggVorbisFile = new Ogg::Vorbis::File(stream); + else if (strExtension == "opus") + file = oggOpusFile = new Ogg::Opus::File(stream); + else if (strExtension == "oga") // Leave this madness until last - oga container can have Vorbis or FLAC + { + file = oggFlacFile = new Ogg::FLAC::File(stream); + if (!file || !file->isValid()) + { + delete file; + oggFlacFile = nullptr; + file = oggVorbisFile = new Ogg::Vorbis::File(stream); + } + } + } + catch (const std::exception& ex) + { + CLog::Log(LOGERROR, "Taglib exception: {}", ex.what()); + } + + if (!file || !file->isOpen()) + { + delete file; + delete stream; + CLog::Log(LOGDEBUG, "file {} could not be opened for tag reading", strFileName); + return false; + } + + APE::Tag *ape = nullptr; + ASF::Tag *asf = nullptr; + MP4::Tag *mp4 = nullptr; + ID3v1::Tag *id3v1 = nullptr; + ID3v2::Tag *id3v2 = nullptr; + Ogg::XiphComment *xiph = nullptr; + Tag *genericTag = nullptr; + + if (apeFile) + ape = apeFile->APETag(false); + else if (asfFile) + asf = asfFile->tag(); + else if (flacFile) + { + xiph = flacFile->xiphComment(false); + id3v2 = flacFile->ID3v2Tag(false); + } + else if (mp4File) + mp4 = mp4File->tag(); + else if (mpegFile) + { + id3v1 = mpegFile->ID3v1Tag(false); + id3v2 = mpegFile->ID3v2Tag(false); + ape = mpegFile->APETag(false); + } + else if (oggFlacFile) + xiph = oggFlacFile->tag(); + else if (oggVorbisFile) + xiph = oggVorbisFile->tag(); + else if (oggOpusFile) + xiph = oggOpusFile->tag(); + else if (ttaFile) + id3v2 = ttaFile->ID3v2Tag(false); + else if (aiffFile) + id3v2 = aiffFile->tag(); + else if (wavFile) + id3v2 = wavFile->ID3v2Tag(); + else if (wvFile) + ape = wvFile->APETag(false); + else if (mpcFile) + ape = mpcFile->APETag(false); + else // This is a catch all to get generic information for other files types (s3m, xm, it, mod, etc) + genericTag = file->tag(); + + if (file->audioProperties()) + { + tag.SetDuration(file->audioProperties()->length()); + tag.SetBitRate(file->audioProperties()->bitrate()); + tag.SetNoOfChannels(file->audioProperties()->channels()); + tag.SetSampleRate(file->audioProperties()->sampleRate()); + } + + if (asf) + ParseTag(asf, art, tag); + if (id3v1) + ParseTag(id3v1, art, tag); + if (id3v2) + ParseTag(id3v2, art, tag); + if (genericTag) + ParseTag(genericTag, art, tag); + if (mp4) + ParseTag(mp4, art, tag); + if (xiph) // xiph tags override id3v2 tags in badly tagged FLACs + ParseTag(xiph, art, tag); + if (ape && (!id3v2 || CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_prioritiseAPEv2tags)) // ape tags override id3v2 if we're prioritising them + ParseTag(ape, art, tag); + + // art for flac files is outside the tag + if (flacFile) + SetFlacArt(flacFile, art, tag); + + if (!tag.GetTitle().empty() || !tag.GetArtist().empty() || !tag.GetAlbum().empty()) + tag.SetLoaded(); + tag.SetURL(strFileName); + + delete file; + delete stream; + + return true; +} diff --git a/xbmc/music/tags/TagLoaderTagLib.h b/xbmc/music/tags/TagLoaderTagLib.h new file mode 100644 index 0000000..a4b82ad --- /dev/null +++ b/xbmc/music/tags/TagLoaderTagLib.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ImusicInfoTagLoader.h" + +#include <string> +#include <vector> + +class EmbeddedArt; + +namespace MUSIC_INFO +{ + class CMusicInfoTag; +}; + +class CTagLoaderTagLib : public MUSIC_INFO::IMusicInfoTagLoader +{ +public: + CTagLoaderTagLib() = default; + ~CTagLoaderTagLib() override = default; + bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag, + EmbeddedArt *art = nullptr) override; + bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag, + const std::string& fallbackFileExtension, EmbeddedArt *art = nullptr); + + static std::vector<std::string> SplitMBID(const std::vector<std::string> &values); +protected: + static void SetArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetAlbumArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetAlbumArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetAlbumArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetComposerSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void SetDiscSubtitle(MUSIC_INFO::CMusicInfoTag& tag, + const std::vector<std::string>& values); + static void SetGenre(MUSIC_INFO::CMusicInfoTag& tag, const std::vector<std::string>& values); + static void SetReleaseType(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values); + static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static void AddArtistInstrument(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values); + static int POPMtoXBMC(int popm); + +template<typename T> + static bool ParseTag(T *tag, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& infoTag); +}; + diff --git a/xbmc/music/tags/test/CMakeLists.txt b/xbmc/music/tags/test/CMakeLists.txt new file mode 100644 index 0000000..e228a9c --- /dev/null +++ b/xbmc/music/tags/test/CMakeLists.txt @@ -0,0 +1,3 @@ +set(SOURCES TestTagLoaderTagLib.cpp) + +core_add_test_library(musictags_test) diff --git a/xbmc/music/tags/test/TestTagLoaderTagLib.cpp b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp new file mode 100644 index 0000000..0818f04 --- /dev/null +++ b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "music/tags/MusicInfoTag.h" +#include "music/tags/TagLoaderTagLib.h" + +#include <gtest/gtest.h> +#include <taglib/apetag.h> +#include <taglib/asftag.h> +#include <taglib/id3v1genres.h> +#include <taglib/id3v1tag.h> +#include <taglib/id3v2tag.h> +#include <taglib/mp4tag.h> +#include <taglib/tpropertymap.h> +#include <taglib/xiphcomment.h> + +using namespace TagLib; +using namespace MUSIC_INFO; + +template <typename T> +class TestTagParser : public ::testing::Test, public CTagLoaderTagLib { + public: + T value_; +}; + + +typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment, MP4::Tag> TagTypes; +TYPED_TEST_SUITE(TestTagParser, TagTypes); + +TYPED_TEST(TestTagParser, ParsesBasicTag) { + // Create a basic tag + TypeParam *tg = &this->value_; + // Configure a basic tag.. + tg->setTitle ("title"); + tg->setArtist ("artist"); + tg->setAlbum ("album"); + tg->setComment("comment"); + tg->setGenre("Jazz"); + tg->setYear (1985); + tg->setTrack (2); + + CMusicInfoTag tag; + EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag)); + + EXPECT_EQ(1985, tag.GetYear()); + EXPECT_EQ(2, tag.GetTrackNumber()); + EXPECT_EQ(1u, tag.GetArtist().size()); + if (!tag.GetArtist().empty()) + { + EXPECT_EQ("artist", tag.GetArtist().front()); + } + EXPECT_EQ("album", tag.GetAlbum()); + EXPECT_EQ("comment", tag.GetComment()); + EXPECT_EQ(1u, tag.GetGenre().size()); + if (!tag.GetGenre().empty()) + { + EXPECT_EQ("Jazz", tag.GetGenre().front()); + } + EXPECT_EQ("title", tag.GetTitle()); +} + + +TYPED_TEST(TestTagParser, HandleNullTag) { + // A Null tag should not parse, and not break us either + CMusicInfoTag tag; + EXPECT_FALSE(CTagLoaderTagLib::ParseTag<TypeParam>(NULL, NULL, tag)); +} + +template<typename T, size_t N> +T * end(T (&ra)[N]) { + return ra + N; +} + +const char *tags[] = { "APIC", "ASPI", "COMM", "COMR", "ENCR", "EQU2", + "ETCO", "GEOB", "GRID", "LINK", "MCDI", "MLLT", "OWNE", "PRIV", "PCNT", + "POPM", "POSS", "RBUF", "RVA2", "RVRB", "SEEK", "SIGN", "SYLT", + "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP", "TDEN", "TDLY", "TDOR", + "TDRC", "TDRL", "TDTG", "TENC", "TEXT", "TFLT", "TIPL", "TIT1", "TIT2", + "TIT3", "TKEY", "TLAN", "TLEN", "TMCL", "TMED", "TMOO", "TOAL", "TOFN", + "TOLY", "TOPE", "TOWN", "TPE1", "TPE2", "TPE3", "TPE4", "TPOS", "TPRO", + "TPUB", "TRCK", "TRSN", "TRSO", "TSOA", "TSOP", "TSOT", "TSRC", "TSSE", + "TSST", "TXXX", "UFID", "USER", "USLT", "WCOM", "WCOP", "WOAF", "WOAR", + "WOAS", "WORS", "WPAY", "WPUB", "WXXX", "ARTIST", "ARTISTS", + "ALBUMARTIST" , "ALBUM ARTIST", "ALBUMARTISTS" , "ALBUM ARTISTS", "ALBUM", + "TITLE", "TRACKNUMBER" "TRACK", "DISCNUMBER" "DISC", "YEAR", "GENRE", + "COMMENT", "CUESHEET", "ENCODEDBY", "COMPILATION", "LYRICS", + "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_TRACK_PEAK", + "REPLAYGAIN_ALBUM_PEAK", "MUSICBRAINZ_ARTISTID", + "MUSICBRAINZ_ALBUMARTISTID", "RATING", "MUSICBRAINZ_ALBUMARTIST", + "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ_TRACKID", "METADATA_BLOCK_PICTURE", + "COVERART" +}; + + +// This test exposes a bug in taglib library (#670) so for now we will not run it for all tag types +// See https://github.com/taglib/taglib/issues/670 for details. +typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment> EmptyPropertiesTagTypes; +template <typename T> +class EmptyTagParser : public ::testing::Test, public CTagLoaderTagLib { + public: + T value_; +}; +TYPED_TEST_SUITE(EmptyTagParser, EmptyPropertiesTagTypes); + +TYPED_TEST(EmptyTagParser, EmptyProperties) { + TypeParam *tg = &this->value_; + CMusicInfoTag tag; + PropertyMap props; + int tagcount = end(tags) - tags; + for(int i = 0; i < tagcount; i++) { + props.insert(tags[i], StringList()); + } + + // Even though all the properties are empty, we shouldn't + // crash + EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag)); +} + + + +TYPED_TEST(TestTagParser, FooProperties) { + TypeParam *tg = &this->value_; + CMusicInfoTag tag; + PropertyMap props; + int tagcount = end(tags) - tags; + for(int i = 0; i < tagcount; i++) { + props.insert(tags[i], String("foo")); + } + tg->setProperties(props); + + EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag)); + EXPECT_EQ(0, tag.GetYear()); + EXPECT_EQ(0, tag.GetTrackNumber()); + EXPECT_EQ(1u, tag.GetArtist().size()); + if (!tag.GetArtist().empty()) + { + EXPECT_EQ("foo", tag.GetArtist().front()); + } + EXPECT_EQ("foo", tag.GetAlbum()); + EXPECT_EQ("foo", tag.GetComment()); + if (!tag.GetGenre().empty()) + { + EXPECT_EQ("foo", tag.GetGenre().front()); + } + EXPECT_EQ("foo", tag.GetTitle()); +} + +class TestTagLoaderTagLib : public ::testing::Test, public CTagLoaderTagLib {}; +TEST_F(TestTagLoaderTagLib, SetGenre) +{ + CMusicInfoTag tag, tag2; + const char *genre_nr[] = {"0", "2", "4"}; + const char *names[] = { "Jazz", "Funk", "Ska" }; + std::vector<std::string> genres(genre_nr, end(genre_nr)); + std::vector<std::string> named_genre(names, end(names)); + + CTagLoaderTagLib::SetGenre(tag, genres); + EXPECT_EQ(3u, tag.GetGenre().size()); + EXPECT_EQ("Blues", tag.GetGenre()[0]); + EXPECT_EQ("Country", tag.GetGenre()[1]); + EXPECT_EQ("Disco", tag.GetGenre()[2]); + + CTagLoaderTagLib::SetGenre(tag2, named_genre); + EXPECT_EQ(3u, tag2.GetGenre().size()); + for(int i = 0; i < 3; i++) + EXPECT_EQ(names[i], tag2.GetGenre()[i]); + +} + +TEST_F(TestTagLoaderTagLib, SplitMBID) +{ + CTagLoaderTagLib lib; + + // SplitMBID should return the vector if it's empty or longer than 1 + std::vector<std::string> values; + EXPECT_TRUE(lib.SplitMBID(values).empty()); + + values.emplace_back("1"); + values.emplace_back("2"); + EXPECT_EQ(values, lib.SplitMBID(values)); + + // length 1 and invalid should return empty + values.clear(); + values.emplace_back("invalid"); + EXPECT_TRUE(lib.SplitMBID(values).empty()); + + // length 1 and valid should return the valid id + values.clear(); + values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + EXPECT_EQ(lib.SplitMBID(values), values); + + // case shouldn't matter + values.clear(); + values.emplace_back("0383DaDf-2A4e-4d10-a46a-e9e041da8eb3"); + EXPECT_EQ(lib.SplitMBID(values).size(), 1u); + EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + + // valid with some stuff off the end or start should return valid + values.clear(); + values.emplace_back("foo0383dadf-2a4e-4d10-a46a-e9e041da8eb3 blah"); + EXPECT_EQ(lib.SplitMBID(values).size(), 1u); + EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + + // two valid with various separators + values.clear(); + values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3;53b106e7-0cc6-42cc-ac95-ed8d30a3a98e"); + std::vector<std::string> result = lib.SplitMBID(values); + EXPECT_EQ(result.size(), 2u); + EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e"); + + values.clear(); + values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3/53b106e7-0cc6-42cc-ac95-ed8d30a3a98e"); + result = lib.SplitMBID(values); + EXPECT_EQ(result.size(), 2u); + EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e"); + + values.clear(); + values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3 / 53b106e7-0cc6-42cc-ac95-ed8d30a3a98e; "); + result = lib.SplitMBID(values); + EXPECT_EQ(result.size(), 2u); + EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"); + EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e"); +} diff --git a/xbmc/music/windows/CMakeLists.txt b/xbmc/music/windows/CMakeLists.txt new file mode 100644 index 0000000..031d735 --- /dev/null +++ b/xbmc/music/windows/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES GUIWindowMusicBase.cpp + GUIWindowMusicNav.cpp + GUIWindowMusicPlaylist.cpp + GUIWindowMusicPlaylistEditor.cpp + GUIWindowVisualisation.cpp + MusicFileItemListModifier.cpp) + +set(HEADERS GUIWindowMusicBase.h + GUIWindowMusicNav.h + GUIWindowMusicPlaylist.h + GUIWindowMusicPlaylistEditor.h + GUIWindowVisualisation.h + MusicFileItemListModifier.h) + +core_add_library(music_windows) diff --git a/xbmc/music/windows/GUIWindowMusicBase.cpp b/xbmc/music/windows/GUIWindowMusicBase.cpp new file mode 100644 index 0000000..df45141 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicBase.cpp @@ -0,0 +1,1097 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowMusicBase.h" + +#include "GUIUserMessages.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogMediaSource.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "music/MusicDbUrl.h" +#include "music/MusicLibraryQueue.h" +#include "music/MusicUtils.h" +#include "music/dialogs/GUIDialogInfoProviderSettings.h" +#include "music/dialogs/GUIDialogMusicInfo.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#ifdef HAS_CDDA_RIPPER +#include "cdrip/CDDARipper.h" +#endif +#include "Autorun.h" +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "GUIPassword.h" +#include "PartyModeManager.h" +#include "URL.h" +#include "addons/gui/GUIDialogAddonInfo.h" +#include "cores/playercorefactory/PlayerCoreFactory.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSmartPlaylistEditor.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "messaging/helpers/DialogHelper.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/tags/MusicInfoTag.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" +#include "video/dialogs/GUIDialogVideoInfo.h" +#include "view/GUIViewState.h" + +#include <algorithm> + +using namespace XFILE; +using namespace MUSICDATABASEDIRECTORY; +using namespace MUSIC_GRABBER; +using namespace MUSIC_INFO; +using namespace KODI::MESSAGING; +using KODI::MESSAGING::HELPERS::DialogResponse; + +using namespace std::chrono_literals; + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_BTNPLAYLISTS 7 +#define CONTROL_BTNSCAN 9 +#define CONTROL_BTNRIP 11 + +CGUIWindowMusicBase::CGUIWindowMusicBase(int id, const std::string &xmlFile) + : CGUIMediaWindow(id, xmlFile.c_str()) +{ + m_dlgProgress = NULL; + m_thumbLoader.SetObserver(this); +} + +CGUIWindowMusicBase::~CGUIWindowMusicBase () = default; + +bool CGUIWindowMusicBase::OnBack(int actionID) +{ + if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + { + CUtil::RemoveTempFiles(); + } + return CGUIMediaWindow::OnBack(actionID); +} + +/*! + \brief Handle messages on window. + \param message GUI Message that can be reacted on. + \return if a message can't be processed, return \e false + + On these messages this class reacts.\n + When retrieving... + - #GUI_MSG_WINDOW_DEINIT\n + ...the last focused control is saved to m_iLastControl. + - #GUI_MSG_WINDOW_INIT\n + ...the musicdatabase is opend and the music extensions and shares are set. + The last focused control is set. + - #GUI_MSG_CLICKED\n + ... the base class reacts on the following controls:\n + Buttons:\n + - #CONTROL_BTNVIEWASICONS - switch between list, thumb and with large items + - #CONTROL_BTNSEARCH - Search for items\n + Other Controls: + - The container controls\n + Have the following actions in message them clicking on them. + - #ACTION_QUEUE_ITEM - add selected item to end of playlist + - #ACTION_QUEUE_ITEM_NEXT - add selected item to next pos in playlist + - #ACTION_SHOW_INFO - retrieve album info from the internet + - #ACTION_SELECT_ITEM - Item has been selected. Overwrite OnClick() to react on it + */ +bool CGUIWindowMusicBase::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + { + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + m_musicdatabase.Close(); + } + break; + + case GUI_MSG_WINDOW_INIT: + { + m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + + m_musicdatabase.Open(); + + if (!CGUIMediaWindow::OnMessage(message)) + return false; + + return true; + } + break; + case GUI_MSG_DIRECTORY_SCANNED: + { + CFileItem directory(message.GetStringParam(), true); + + // Only update thumb on a local drive + if (directory.IsHD()) + { + std::string strParent; + URIUtils::GetParentPath(directory.GetPath(), strParent); + if (directory.GetPath() == m_vecItems->GetPath() || strParent == m_vecItems->GetPath()) + Refresh(); + } + } + break; + + // update the display + case GUI_MSG_SCAN_FINISHED: + case GUI_MSG_REFRESH_THUMBS: // Never called as is secondary msg sent as GUI_MSG_NOTIFY_ALL + Refresh(); + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNRIP) + { + OnRipCD(); + } + else if (iControl == CONTROL_BTNPLAYLISTS) + { + if (!m_vecItems->IsPath("special://musicplaylists/")) + Update("special://musicplaylists/"); + } + else if (iControl == CONTROL_BTNSCAN) + { + OnScan(-1); + } + else if (m_viewControl.HasControl(iControl)) // list/thumb control + { + int iItem = m_viewControl.GetSelectedItem(); + int iAction = message.GetParam1(); + + // iItem is checked for validity inside these routines + if (iAction == ACTION_QUEUE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK) + { + OnQueueItem(iItem); + } + else if (iAction == ACTION_QUEUE_ITEM_NEXT) + { + OnQueueItem(iItem, true); + } + else if (iAction == ACTION_SHOW_INFO) + { + OnItemInfo(iItem); + } + else if (iAction == ACTION_DELETE_ITEM) + { + // is delete allowed? + // must be at the playlists directory + if (m_vecItems->IsPath("special://musicplaylists/")) + OnDeleteItem(iItem); + + else + return false; + } + // use play button to add folders of items to temp playlist + else if (iAction == ACTION_PLAYER_PLAY) + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + // if playback is paused or playback speed != 1, return + if (appPlayer->IsPlayingAudio()) + { + if (appPlayer->IsPausedPlayback()) + return false; + if (appPlayer->GetPlaySpeed() != 1) + return false; + } + + // not playing audio, or playback speed == 1 + PlayItem(iItem); + + return true; + } + } + } + break; + case GUI_MSG_NOTIFY_ALL: + { + if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA) + CUtil::DeleteDirectoryCache("r-"); + } + break; + } + return CGUIMediaWindow::OnMessage(message); +} + +bool CGUIWindowMusicBase::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_SHOW_PLAYLIST) + { + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC || + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size() > 0) + { + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST); + return true; + } + } + + if (action.GetID() == ACTION_SCAN_ITEM) + { + int item = m_viewControl.GetSelectedItem(); + if (item > -1 && m_vecItems->Get(item)->m_bIsFolder) + OnScan(item); + + return true; + } + + return CGUIMediaWindow::OnAction(action); +} + +void CGUIWindowMusicBase::OnItemInfoAll(const std::string& strPath, bool refresh) +{ + if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums")) + { + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + return; + + CMusicLibraryQueue::GetInstance().StartAlbumScan(strPath, refresh); + } + else if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists")) + { + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + return; + + CMusicLibraryQueue::GetInstance().StartArtistScan(strPath, refresh); + } +} + +void CGUIWindowMusicBase::OnItemInfo(int iItem) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size() ) + return; + + CFileItemPtr item = m_vecItems->Get(iItem); + + // Match visibility test of CMusicInfo::IsVisible + if (item->IsVideoDb() && item->HasVideoInfoTag() && + (item->HasProperty("artist_musicid") || item->HasProperty("album_musicid"))) + { + // Music video artist or album (navigation by music > music video > artist)) + CGUIDialogMusicInfo::ShowFor(item.get()); + return; + } + + if (item->IsVideo() && item->HasVideoInfoTag() && + item->GetVideoInfoTag()->m_type == MediaTypeMusicVideo) + { // Music video on a mixed current playlist or navigation by music > music video > artist > video + CGUIDialogVideoInfo::ShowFor(*item); + return; + } + + if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript())) + { + CGUIDialogAddonInfo::ShowForItem(item); + return; + } + + // Match visibility test of CMusicInfo::IsVisible + if (item->HasMusicInfoTag() && (item->GetMusicInfoTag()->GetType() == MediaTypeSong || + item->GetMusicInfoTag()->GetType() == MediaTypeAlbum || + item->GetMusicInfoTag()->GetType() == MediaTypeArtist)) + CGUIDialogMusicInfo::ShowFor(item.get()); +} + +void CGUIWindowMusicBase::RefreshContent(const std::string& strContent) +{ + if ( CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV && + m_vecItems->GetContent() == strContent && + m_vecItems->GetSortMethod() == SortByUserRating) + // When music library window is active and showing songs or albums sorted + // by userrating refresh the list to resort items and show new userrating + Refresh(true); +} + +/// \brief Retrieve tag information for \e m_vecItems +void CGUIWindowMusicBase::RetrieveMusicInfo() +{ + auto start = std::chrono::steady_clock::now(); + + OnRetrieveMusicInfo(*m_vecItems); + + //! @todo Scan for multitrack items here... + std::vector<std::string> itemsForRemove; + CFileItemList itemsForAdd; + for (int i = 0; i < m_vecItems->Size(); ++i) + { + CFileItemPtr pItem = (*m_vecItems)[i]; + if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics() || pItem->IsVideo()) + continue; + + CMusicInfoTag& tag = *pItem->GetMusicInfoTag(); + if (tag.Loaded() && !tag.GetCueSheet().empty()) + pItem->LoadEmbeddedCue(); + + if (pItem->HasCueDocument() + && pItem->LoadTracksFromCueDocument(itemsForAdd)) + { + itemsForRemove.push_back(pItem->GetPath()); + } + } + for (size_t i = 0; i < itemsForRemove.size(); ++i) + { + for (int j = 0; j < m_vecItems->Size(); ++j) + { + if ((*m_vecItems)[j]->GetPath() == itemsForRemove[i]) + { + m_vecItems->Remove(j); + break; + } + } + } + m_vecItems->Append(itemsForAdd); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "RetrieveMusicInfo() took {} ms", duration.count()); +} + +/// \brief Add selected list/thumb control item to playlist and start playing +/// \param iItem Selected Item in list/thumb control +void CGUIWindowMusicBase::OnQueueItem(int iItem, bool first) +{ + // don't re-queue items from playlist window + if (iItem < 0 || iItem >= m_vecItems->Size() || GetID() == WINDOW_MUSIC_PLAYLIST) + return; + + // add item 2 playlist + const auto item = m_vecItems->Get(iItem); + + if (item->IsRAR() || item->IsZIP()) + return; + + MUSIC_UTILS::QueueItem(item, first ? MUSIC_UTILS::QueuePosition::POSITION_BEGIN + : MUSIC_UTILS::QueuePosition::POSITION_END); + + // select next item + m_viewControl.SetSelectedItem(iItem + 1); +} + +void CGUIWindowMusicBase::UpdateButtons() +{ + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNRIP, CServiceBroker::GetMediaManager().IsAudio()); + + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNSCAN, + !(m_vecItems->IsVirtualDirectoryRoot() || + m_vecItems->IsMusicDb())); + + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + SET_CONTROL_LABEL(CONTROL_BTNSCAN, 14056); // Stop Scan + else + SET_CONTROL_LABEL(CONTROL_BTNSCAN, 102); // Scan + + CGUIMediaWindow::UpdateButtons(); +} + +void CGUIWindowMusicBase::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + if (item) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // Check for the partymode playlist item. + // When "PartyMode.xsp" not exist, only context menu button is edit + if (item->IsSmartPlayList() && + (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) && + !CFileUtils::Exists(item->GetPath())) + { + buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586); + return; + } + + if (!item->IsParentFolder()) + { + //! @todo get rid of IsAddonsPath and IsScript check. CanQueue should be enough! + if (item->CanQueue() && !item->IsAddonsPath() && !item->IsScript()) + { + if (!item->m_bIsFolder && + (!item->IsPlayList() || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)) + { + const CPlayerCoreFactory& playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + // check what players we have, if we have multiple display play with option + std::vector<std::string> players; + playerCoreFactory.GetPlayers(*item, players); + if (players.size() >= 1) + buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With... + } + + if (item->IsSmartPlayList()) + buttons.Add(CONTEXT_BUTTON_PLAY_PARTYMODE, 15216); // Play in Partymode + + if (item->IsSmartPlayList() || m_vecItems->IsSmartPlayList()) + buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586); + else if (item->IsPlayList() || m_vecItems->IsPlayList()) + buttons.Add(CONTEXT_BUTTON_EDIT, 586); + } +#ifdef HAS_DVD_DRIVE + // enable Rip CD Audio or Track button if we have an audio disc + if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA()) + { + // those cds can also include Audio Tracks: CDExtra and MixedMode! + MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(); + if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1)) + buttons.Add(CONTEXT_BUTTON_RIP_TRACK, 610); + } +#endif + } + + // enable CDDB lookup if the current dir is CDDA + if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA() && + (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser)) + { + buttons.Add(CONTEXT_BUTTON_CDDB, 16002); + } + } + CGUIMediaWindow::GetContextButtons(itemNumber, buttons); +} + +void CGUIWindowMusicBase::GetNonContextButtons(CContextButtons &buttons) +{ +} + +bool CGUIWindowMusicBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + if (CGUIDialogContextMenu::OnContextButton("music", item, button)) + { + if (button == CONTEXT_BUTTON_REMOVE_SOURCE) + OnRemoveSource(itemNumber); + + Update(m_vecItems->GetPath()); + return true; + } + + switch (button) + { + case CONTEXT_BUTTON_INFO: + OnItemInfo(itemNumber); + return true; + + case CONTEXT_BUTTON_EDIT: + { + std::string playlist = item->IsPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST_EDITOR, playlist); + // need to update + m_vecItems->RemoveDiscCache(GetID()); + return true; + } + + case CONTEXT_BUTTON_EDIT_SMART_PLAYLIST: + { + std::string playlist = item->IsSmartPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items + if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist, "music")) + Refresh(true); // need to update + return true; + } + + case CONTEXT_BUTTON_PLAY_WITH: + { + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + std::vector<std::string> players; + playerCoreFactory.GetPlayers(*item, players); + std::string player = playerCoreFactory.SelectPlayerDialog(players); + if (!player.empty()) + OnClick(itemNumber, player); + return true; + } + + case CONTEXT_BUTTON_PLAY_PARTYMODE: + g_partyModeManager.Enable(PARTYMODECONTEXT_MUSIC, item->GetPath()); + return true; + + case CONTEXT_BUTTON_RIP_CD: + OnRipCD(); + return true; + +#ifdef HAS_CDDA_RIPPER + case CONTEXT_BUTTON_CANCEL_RIP_CD: + KODI::CDRIP::CCDDARipper::GetInstance().CancelJobs(); + return true; +#endif + + case CONTEXT_BUTTON_RIP_TRACK: + OnRipTrack(itemNumber); + return true; + + case CONTEXT_BUTTON_SCAN: + // Check if scanning already and inform user + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 }); + else + OnScan(itemNumber, true); + return true; + + case CONTEXT_BUTTON_CDDB: + if (m_musicdatabase.LookupCDDBInfo(true)) + Refresh(); + return true; + + default: + break; + } + + return CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +bool CGUIWindowMusicBase::OnAddMediaSource() +{ + return CGUIDialogMediaSource::ShowAndAddMediaSource("music"); +} + +void CGUIWindowMusicBase::OnRipCD() +{ + if (CServiceBroker::GetMediaManager().IsAudio()) + { + if (!g_application.CurrentFileItem().IsCDDA()) + { +#ifdef HAS_CDDA_RIPPER + KODI::CDRIP::CCDDARipper::GetInstance().RipCD(); +#endif + } + else + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099}); + } +} + +void CGUIWindowMusicBase::OnRipTrack(int iItem) +{ + if (CServiceBroker::GetMediaManager().IsAudio()) + { + if (!g_application.CurrentFileItem().IsCDDA()) + { +#ifdef HAS_CDDA_RIPPER + CFileItemPtr item = m_vecItems->Get(iItem); + KODI::CDRIP::CCDDARipper::GetInstance().RipTrack(item.get()); +#endif + } + else + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099}); + } +} + +void CGUIWindowMusicBase::PlayItem(int iItem) +{ + // restrictions should be placed in the appropriate window code + // only call the base code if the item passes since this clears + // the current playlist + + const CFileItemPtr pItem = m_vecItems->Get(iItem); +#ifdef HAS_DVD_DRIVE + if (pItem->IsDVD()) + { + MEDIA_DETECT::CAutorun::PlayDiscAskResume(pItem->GetPath()); + return; + } +#endif + + // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exist + if (pItem->IsSmartPlayList()) + { + const std::shared_ptr<CProfileManager> profileManager = + CServiceBroker::GetSettingsComponent()->GetProfileManager(); + if ((pItem->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) && + !CFileUtils::Exists(pItem->GetPath())) + return; + } + + // if its a folder, build a playlist + if (pItem->m_bIsFolder && !pItem->IsPlugin()) + { + // make a copy so that we can alter the queue state + CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem))); + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!item->CanQueue()) + item->SetCanQueue(true); + + // skip ".." + if (item->IsParentFolder()) + return; + + CFileItemList queuedItems; + MUSIC_UTILS::GetItemsForPlayList(item, queuedItems); + if (g_partyModeManager.IsEnabled()) + { + g_partyModeManager.AddUserSongs(queuedItems, true); + return; + } + + /* + std::string strPlayListDirectory = m_vecItems->GetPath(); + URIUtils::RemoveSlashAtEnd(strPlayListDirectory); + */ + + CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC); + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_MUSIC, queuedItems); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC); + + // play! + CServiceBroker::GetPlaylistPlayer().Play(); + } + else if (pItem->IsPlayList()) + { + // load the playlist the old way + LoadPlayList(pItem->GetPath()); + } + else + { + // just a single item, play it + //! @todo Add music-specific code for single playback of an item here (See OnClick in MediaWindow, and OnPlayMedia below) + OnClick(iItem); + } +} + +void CGUIWindowMusicBase::LoadPlayList(const std::string& strPlayList) +{ + // if partymode is active, we disable it + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Disable(); + + // load a playlist like .m3u, .pls + // first get correct factory to load playlist + std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList)); + if (pPlayList) + { + // load it + if (!pPlayList->Load(strPlayList)) + { + HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477}); + return; //hmmm unable to load playlist? + } + } + + int iSize = pPlayList->size(); + if (g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, PLAYLIST::TYPE_MUSIC)) + { + if (m_guiState) + m_guiState->SetPlaylistDirectory("playlistmusic://"); + // activate the playlist window if its not activated yet + if (GetID() == CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() && iSize > 1) + { + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST); + } + } +} + +bool CGUIWindowMusicBase::OnPlayMedia(int iItem, const std::string &player) +{ + CFileItemPtr pItem = m_vecItems->Get(iItem); + + // party mode + if (g_partyModeManager.IsEnabled()) + { + PLAYLIST::CPlayList playlistTemp; + playlistTemp.Add(pItem); + g_partyModeManager.AddUserSongs(playlistTemp, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT)); + return true; + } + else if (!pItem->IsPlayList() && !pItem->IsInternetStream()) + { // single music file - if we get here then we have autoplaynextitem turned off or queuebydefault + // turned on, but we still want to use the playlist player in order to handle more queued items + // following etc. + if ( (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT) && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST_EDITOR) ) + { + //! @todo Should the playlist be cleared if nothing is already playing? + OnQueueItem(iItem); + return true; + } + pItem->SetProperty("playlist_type_hint", m_guiState->GetPlaylist()); + CServiceBroker::GetPlaylistPlayer().Play(pItem, player); + return true; + } + return CGUIMediaWindow::OnPlayMedia(iItem, player); +} + +/// \brief Can be overwritten to implement an own tag filling function. +/// \param items File items to fill +void CGUIWindowMusicBase::OnRetrieveMusicInfo(CFileItemList& items) +{ + // No need to attempt to read music file tags for music videos + if (items.IsVideoDb()) + return; + if (items.GetFolderCount()==items.Size() || items.IsMusicDb() || + (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) && !items.IsCDDA())) + { + return; + } + // Start the music info loader thread + m_musicInfoLoader.SetProgressCallback(m_dlgProgress); + m_musicInfoLoader.Load(items); + + bool bShowProgress = !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true); + bool bProgressVisible = false; + + auto start = std::chrono::steady_clock::now(); + + while (m_musicInfoLoader.IsLoading()) + { + if (bShowProgress) + { // Do we have to init a progress dialog? + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + if (!bProgressVisible && duration.count() > 1500 && m_dlgProgress) + { // tag loading takes more then 1.5 secs, show a progress dialog + CURL url(items.GetPath()); + m_dlgProgress->SetHeading(CVariant{189}); + m_dlgProgress->SetLine(0, CVariant{505}); + m_dlgProgress->SetLine(1, CVariant{""}); + m_dlgProgress->SetLine(2, CVariant{url.GetWithoutUserDetails()}); + m_dlgProgress->Open(); + m_dlgProgress->ShowProgressBar(true); + bProgressVisible = true; + } + + if (bProgressVisible && m_dlgProgress && !m_dlgProgress->IsCanceled()) + { // keep GUI alive + m_dlgProgress->Progress(); + } + } // if (bShowProgress) + KODI::TIME::Sleep(1ms); + } // while (m_musicInfoLoader.IsLoading()) + + if (bProgressVisible && m_dlgProgress) + m_dlgProgress->Close(); +} + +bool CGUIWindowMusicBase::GetDirectory(const std::string &strDirectory, CFileItemList &items) +{ + items.ClearArt(); + bool bResult = CGUIMediaWindow::GetDirectory(strDirectory, items); + if (bResult) + { + // We want to expand disc images when browsing in file view but not on library, smartplaylist + // or node menu music windows + if (!items.GetPath().empty() && !StringUtils::StartsWithNoCase(items.GetPath(), "musicdb://") && + !StringUtils::StartsWithNoCase(items.GetPath(), "special://") && + !StringUtils::StartsWithNoCase(items.GetPath(), "library://")) + CDirectory::FilterFileDirectories(items, ".iso", true); + + CMusicThumbLoader loader; + loader.FillThumb(items); + + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(items.GetPath(), params); + + // Get art for directory when album or artist + bool artfound = false; + std::vector<ArtForThumbLoader> art; + if (params.GetAlbumId() > 0) + { // Get album and related artist(s) art + artfound = m_musicdatabase.GetArtForItem(-1, params.GetAlbumId(), -1, false, art); + } + else if (params.GetArtistId() > 0) + { // get artist art + artfound = m_musicdatabase.GetArtForItem(-1, -1, params.GetArtistId(), true, art); + } + if (artfound) + { + std::string dirType = MediaTypeArtist; + if (params.GetAlbumId() > 0) + dirType = MediaTypeAlbum; + std::map<std::string, std::string> artmap; + for (auto artitem : art) + { + std::string artname; + if (dirType == artitem.mediaType) + artname = artitem.artType; + else if (artitem.prefix.empty()) + artname = artitem.mediaType + "." + artitem.artType; + else + { + if (dirType == MediaTypeAlbum) + StringUtils::Replace(artitem.prefix, "albumartist", "artist"); + artname = artitem.prefix + "." + artitem.artType; + } + artmap.insert(std::make_pair(artname, artitem.url)); + } + items.SetArt(artmap); + } + + int iWindow = GetID(); + // Add "New Playlist" items when in the playlists folder, except on playlist editor screen + if ((iWindow != WINDOW_MUSIC_PLAYLIST_EDITOR) && + (items.GetPath() == "special://musicplaylists/") && !items.Contains("newplaylist://")) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + CFileItemPtr newPlaylist(new CFileItem(profileManager->GetUserDataItem("PartyMode.xsp"),false)); + newPlaylist->SetLabel(g_localizeStrings.Get(16035)); + newPlaylist->SetLabelPreformatted(true); + newPlaylist->SetArt("icon", "DefaultPartyMode.png"); + newPlaylist->m_bIsFolder = true; + items.Add(newPlaylist); + + newPlaylist.reset(new CFileItem("newplaylist://", false)); + newPlaylist->SetLabel(g_localizeStrings.Get(525)); + newPlaylist->SetArt("icon", "DefaultAddSource.png"); + newPlaylist->SetLabelPreformatted(true); + newPlaylist->SetSpecialSort(SortSpecialOnBottom); + newPlaylist->SetCanQueue(false); + items.Add(newPlaylist); + + newPlaylist.reset(new CFileItem("newsmartplaylist://music", false)); + newPlaylist->SetLabel(g_localizeStrings.Get(21437)); + newPlaylist->SetArt("icon", "DefaultAddSource.png"); + newPlaylist->SetLabelPreformatted(true); + newPlaylist->SetSpecialSort(SortSpecialOnBottom); + newPlaylist->SetCanQueue(false); + items.Add(newPlaylist); + } + + // check for .CUE files here. + items.FilterCueItems(); + + std::string label; + if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("music"), &label)) + items.SetLabel(label); + } + + return bResult; +} + +bool CGUIWindowMusicBase::CheckFilterAdvanced(CFileItemList &items) const +{ + const std::string& content = items.GetContent(); + if ((items.IsMusicDb() || CanContainFilter(m_strFilterPath)) && + (StringUtils::EqualsNoCase(content, "artists") || + StringUtils::EqualsNoCase(content, "albums") || + StringUtils::EqualsNoCase(content, "songs"))) + return true; + + return false; +} + +bool CGUIWindowMusicBase::CanContainFilter(const std::string &strDirectory) const +{ + return URIUtils::IsProtocol(strDirectory, "musicdb"); +} + +bool CGUIWindowMusicBase::OnSelect(int iItem) +{ + auto item = m_vecItems->Get(iItem); + if (item->IsAudioBook()) + { + int bookmark; + if (m_musicdatabase.GetResumeBookmarkForAudioBook(*item, bookmark) && bookmark > 0) + { + // find which chapter the bookmark belongs to + auto itemIt = + std::find_if(m_vecItems->cbegin(), m_vecItems->cend(), + [&](const CFileItemPtr& item) { return bookmark < item->GetEndOffset(); }); + + if (itemIt != m_vecItems->cend()) + { + // ask the user if they want to play or resume + CContextButtons choices; + choices.Add(MUSIC_SELECT_ACTION_PLAY, 208); // 208 = Play + choices.Add(MUSIC_SELECT_ACTION_RESUME, + StringUtils::Format(g_localizeStrings.Get(12022), // 12022 = Resume from ... + (*itemIt)->GetMusicInfoTag()->GetTitle())); + + auto choice = CGUIDialogContextMenu::Show(choices); + if (choice == MUSIC_SELECT_ACTION_RESUME) + { + (*itemIt)->SetProperty("audiobook_bookmark", bookmark); + return CGUIMediaWindow::OnSelect(static_cast<int>(itemIt - m_vecItems->cbegin())); + } + else if (choice < 0) + return true; + } + } + } + + return CGUIMediaWindow::OnSelect(iItem); +} + +void CGUIWindowMusicBase::OnInitWindow() +{ + CGUIMediaWindow::OnInitWindow(); + // Prompt for rescan of library to read music file tags that were not processed by previous versions + // and accommodate any changes to the way some tags are processed + if (m_musicdatabase.GetMusicNeedsTagScan() != 0) + { + if (CServiceBroker::GetGUI() + ->GetInfoManager() + .GetInfoProviders() + .GetLibraryInfoProvider() + .GetLibraryBool(LIBRARY_HAS_MUSIC) && + !CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + { + // rescan of music library required + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38060})) + { + int flags = CMusicInfoScanner::SCAN_RESCAN; + // When set to fetch information on update enquire about scraping that as well + // It may take some time, so the user may want to do it later by "Query Info For All" + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO)) + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38061})) + flags |= CMusicInfoScanner::SCAN_ONLINE; + + CMusicLibraryQueue::GetInstance().ScanLibrary("", flags, true); + + m_musicdatabase.SetMusicTagScanVersion(); // once is enough (user may interrupt, but that's up to them) + } + } + else + { + // no need to force a rescan if there's no music in the library or if a library scan is already active + m_musicdatabase.SetMusicTagScanVersion(); + } + } +} + +std::string CGUIWindowMusicBase::GetStartFolder(const std::string &dir) +{ + std::string lower(dir); StringUtils::ToLower(lower); + if (lower == "plugins" || lower == "addons") + return "addons://sources/audio/"; + else if (lower == "$playlists" || lower == "playlists") + return "special://musicplaylists/"; + return CGUIMediaWindow::GetStartFolder(dir); +} + +void CGUIWindowMusicBase::OnScan(int iItem, bool bPromptRescan /*= false*/) +{ + std::string strPath; + if (iItem < 0 || iItem >= m_vecItems->Size()) + strPath = m_vecItems->GetPath(); + else if (m_vecItems->Get(iItem)->m_bIsFolder) + strPath = m_vecItems->Get(iItem)->GetPath(); + else + { //! @todo MUSICDB - should we allow scanning a single item into the database? + //! This will require changes to the info scanner, which assumes we're running on a folder + strPath = m_vecItems->GetPath(); + } + // Ask for full rescan of music files when scan item from file view context menu + bool doRescan = false; + if (bPromptRescan) + doRescan = CGUIDialogYesNo::ShowAndGetInput(CVariant{ 799 }, CVariant{ 38062 }); + + DoScan(strPath, doRescan); +} + +void CGUIWindowMusicBase::DoScan(const std::string &strPath, bool bRescan /*= false*/) +{ + if (CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + { + CMusicLibraryQueue::GetInstance().StopLibraryScanning(); + return; + } + + // Start background loader + int iControl=GetFocusedControlID(); + int flags = 0; + if (bRescan) + flags = CMusicInfoScanner::SCAN_RESCAN; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO)) + flags |= CMusicInfoScanner::SCAN_ONLINE; + + CMusicLibraryQueue::GetInstance().ScanLibrary(strPath, flags, true); + + SET_CONTROL_FOCUS(iControl, 0); + UpdateButtons(); +} + +void CGUIWindowMusicBase::OnRemoveSource(int iItem) +{ + + //Remove music source from library, even when leaving songs + CMusicDatabase database; + database.Open(); + database.RemoveSource(m_vecItems->Get(iItem)->GetLabel()); + + bool bCanceled; + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{522}, CVariant{20340}, bCanceled, CVariant{""}, CVariant{""}, CGUIDialogYesNo::NO_TIMEOUT)) + { + MAPSONGS songs; + database.RemoveSongsFromPath(m_vecItems->Get(iItem)->GetPath(), songs, false); + database.CleanupOrphanedItems(); + database.CheckArtistLinksChanged(); + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools(); + m_vecItems->RemoveDiscCache(GetID()); + } + database.Close(); +} + +void CGUIWindowMusicBase::OnPrepareFileItems(CFileItemList &items) +{ + CGUIMediaWindow::OnPrepareFileItems(items); + + if (!items.IsMusicDb() && !items.IsSmartPlayList()) + RetrieveMusicInfo(); +} + +void CGUIWindowMusicBase::OnAssignContent(const std::string& oldName, const CMediaSource& source) +{ + // Music scrapers are not source specific, so unlike video there is no content selection logic here. + // Called on having added or edited a music source, this starts scanning items into library when required + + //! @todo: do async as updating sources for all albums could be slow?? + //Store music source in the music library, even those not scanned + CMusicDatabase database; + database.Open(); + database.UpdateSource(oldName, source.strName, source.strPath, source.vecPaths); + database.Close(); + + // "Add to library" yes/no dialog with additional "settings" custom button + // "Do you want to add the media from this source to your library?" + DialogResponse rep = DialogResponse::CHOICE_CUSTOM; + while (rep == DialogResponse::CHOICE_CUSTOM) + { + rep = HELPERS::ShowYesNoCustomDialog(CVariant{20444}, CVariant{20447}, CVariant{106}, CVariant{107}, CVariant{10004}); + if (rep == DialogResponse::CHOICE_CUSTOM) + // Edit default info provider settings so can be applied during scan + CGUIDialogInfoProviderSettings::Show(); + } + if (rep == DialogResponse::CHOICE_YES) + CMusicLibraryQueue::GetInstance().ScanLibrary(source.strPath, + MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL, true); +} + diff --git a/xbmc/music/windows/GUIWindowMusicBase.h b/xbmc/music/windows/GUIWindowMusicBase.h new file mode 100644 index 0000000..85f4931 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicBase.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +/*! +\file GUIWindowMusicBase.h +\brief +*/ + +#include "music/MusicDatabase.h" +#include "music/MusicInfoLoader.h" +#include "music/MusicThumbLoader.h" +#include "music/infoscanner/MusicInfoScraper.h" +#include "windows/GUIMediaWindow.h" + +#include <vector> + +enum MusicSelectAction +{ + MUSIC_SELECT_ACTION_PLAY, + MUSIC_SELECT_ACTION_RESUME, +}; + +/*! + \ingroup windows + \brief The base class for music windows + + CGUIWindowMusicBase is the base class for + all music windows. + */ +class CGUIWindowMusicBase : public CGUIMediaWindow, public IBackgroundLoaderObserver +{ +public: + CGUIWindowMusicBase(int id, const std::string &xmlFile); + ~CGUIWindowMusicBase(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + bool OnBack(int actionID) override; + + void DoScan(const std::string &strPath, bool bRescan = false); + void RefreshContent(const std::string& strContent); + + /*! \brief Once a music source is added, store source in library, and prompt + the user to scan this folder into the library + \param oldName the original music source name + \param source details of the music source (just added or edited) + */ + static void OnAssignContent(const std::string& oldName, const CMediaSource& source); + +protected: + void OnInitWindow() override; + /*! + \brief Will be called when an popup context menu has been asked for + \param itemNumber List/thumb control item that has been clicked on + */ + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + void GetNonContextButtons(CContextButtons &buttons); + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool OnAddMediaSource() override; + /*! + \brief Overwrite to update your gui buttons (visible, enable,...) + */ + void UpdateButtons() override; + + bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override; + virtual void OnRetrieveMusicInfo(CFileItemList& items); + void OnPrepareFileItems(CFileItemList& items) override; + void OnRipCD(); + std::string GetStartFolder(const std::string &dir) override; + void OnItemLoaded(CFileItem* pItem) override {} + + virtual void OnScan(int iItem, bool bPromptRescan = false); + + bool CheckFilterAdvanced(CFileItemList &items) const override; + bool CanContainFilter(const std::string &strDirectory) const override; + + bool OnSelect(int iItem) override; + + // new methods + virtual void PlayItem(int iItem); + bool OnPlayMedia(int iItem, const std::string &player = "") override; + + void RetrieveMusicInfo(); + void OnItemInfo(int iItem); + void OnItemInfoAll(const std::string& strPath, bool refresh = false); + virtual void OnQueueItem(int iItem, bool first = false); + enum ALLOW_SELECTION { SELECTION_ALLOWED = 0, SELECTION_AUTO, SELECTION_FORCED }; + + void OnRipTrack(int iItem); + void LoadPlayList(const std::string& strPlayList) override; + virtual void OnRemoveSource(int iItem); + + typedef std::vector <CFileItem*>::iterator ivecItems; ///< CFileItem* vector Iterator + CGUIDialogProgress* m_dlgProgress; ///< Progress dialog + + CMusicDatabase m_musicdatabase; + MUSIC_INFO::CMusicInfoLoader m_musicInfoLoader; + + CMusicThumbLoader m_thumbLoader; +}; diff --git a/xbmc/music/windows/GUIWindowMusicNav.cpp b/xbmc/music/windows/GUIWindowMusicNav.cpp new file mode 100644 index 0000000..f8ba5a4 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicNav.cpp @@ -0,0 +1,944 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowMusicNav.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "PartyModeManager.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "addons/AddonSystemSettings.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/MusicDatabaseDirectory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIEditControl.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicLibraryQueue.h" +#include "music/dialogs/GUIDialogInfoProviderSettings.h" +#include "music/tags/MusicInfoTag.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/LegacyPathTranslation.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/dialogs/GUIDialogVideoInfo.h" +#include "video/windows/GUIWindowVideoNav.h" +#include "view/GUIViewState.h" + +using namespace XFILE; +using namespace PLAYLIST; +using namespace MUSICDATABASEDIRECTORY; +using namespace KODI::MESSAGING; + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_BTNTYPE 5 +#define CONTROL_LABELFILES 12 + +#define CONTROL_SEARCH 8 +#define CONTROL_FILTER 15 +#define CONTROL_BTNPARTYMODE 16 +#define CONTROL_BTNMANUALINFO 17 +#define CONTROL_BTN_FILTER 19 + +#define CONTROL_UPDATE_LIBRARY 20 + +CGUIWindowMusicNav::CGUIWindowMusicNav(void) + : CGUIWindowMusicBase(WINDOW_MUSIC_NAV, "MyMusicNav.xml") +{ + m_vecItems->SetPath("?"); + m_searchWithEdit = false; +} + +CGUIWindowMusicNav::~CGUIWindowMusicNav(void) = default; + +bool CGUIWindowMusicNav::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_RESET: + m_vecItems->SetPath("?"); + break; + case GUI_MSG_WINDOW_INIT: + { + /* We don't want to show Autosourced items (ie removable pendrives, memorycards) in Library mode */ + m_rootDir.AllowNonLocalSources(false); + + // is this the first time the window is opened? + if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty()) + message.SetStringParam(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW)); + + if (!CGUIWindowMusicBase::OnMessage(message)) + return false; + + if (message.GetStringParam(0) != "") + { + CURL url(message.GetStringParam(0)); + + int i = 0; + for (; i < m_vecItems->Size(); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + + // skip ".." + if (pItem->IsParentFolder()) + continue; + + if (URIUtils::PathEquals(pItem->GetPath(), message.GetStringParam(0), true, true)) + { + m_viewControl.SetSelectedItem(i); + i = -1; + if (url.GetOption("showinfo") == "true") + OnItemInfo(i); + break; + } + } + } + + return true; + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNPARTYMODE) + { + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Disable(); + else + { + if (!g_partyModeManager.Enable()) + { + SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE,false); + return false; + } + + // Playlist directory is the root of the playlist window + if (m_guiState) + m_guiState->SetPlaylistDirectory("playlistmusic://"); + + return true; + } + UpdateButtons(); + } + else if (iControl == CONTROL_SEARCH) + { + if (m_searchWithEdit) + { + // search updated - reset timer + m_searchTimer.StartZero(); + // grab our search string + CGUIMessage selected(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SEARCH); + OnMessage(selected); + SetProperty("search", selected.GetLabel()); + return true; + } + std::string search(GetProperty("search").asString()); + CGUIKeyboardFactory::ShowAndGetFilter(search, true); + SetProperty("search", search); + return true; + } + else if (iControl == CONTROL_UPDATE_LIBRARY) + { + if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary()) + CMusicLibraryQueue::GetInstance().ScanLibrary(""); + else + CMusicLibraryQueue::GetInstance().StopLibraryScanning(); + return true; + } + } + break; + case GUI_MSG_PLAYBACK_STOPPED: + case GUI_MSG_PLAYBACK_ENDED: + case GUI_MSG_PLAYLISTPLAYER_STOPPED: + case GUI_MSG_PLAYBACK_STARTED: + { + SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled()); + } + break; + case GUI_MSG_NOTIFY_ALL: + { + if (message.GetParam1() == GUI_MSG_SEARCH_UPDATE && IsActive()) + { + // search updated - reset timer + m_searchTimer.StartZero(); + SetProperty("search", message.GetStringParam()); + } + } + } + return CGUIWindowMusicBase::OnMessage(message); +} + +bool CGUIWindowMusicNav::OnAction(const CAction& action) +{ + if (action.GetID() == ACTION_SCAN_ITEM) + { + int item = m_viewControl.GetSelectedItem(); + CMusicDatabaseDirectory dir; + if (item > -1 && m_vecItems->Get(item)->m_bIsFolder + && (m_vecItems->Get(item)->IsAlbum()|| + dir.IsArtistDir(m_vecItems->Get(item)->GetPath()))) + { + OnContextButton(item,CONTEXT_BUTTON_INFO); + return true; + } + } + + return CGUIWindowMusicBase::OnAction(action); +} + +bool CGUIWindowMusicNav::ManageInfoProvider(const CFileItemPtr& item) +{ + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(item->GetPath(), params); + // Management of Info provider only valid for specific artist or album items + if (params.GetAlbumId() == -1 && params.GetArtistId() == -1) + return false; + + // Set things up for processing artist or albums + CONTENT_TYPE content = CONTENT_ALBUMS; + int id = params.GetAlbumId(); + if (id == -1) + { + content = CONTENT_ARTISTS; + id = params.GetArtistId(); + } + + ADDON::ScraperPtr scraper; + // Get specific scraper and settings for current item or use default + if (!m_musicdatabase.GetScraper(id, content, scraper)) + { + ADDON::AddonPtr defaultScraper; + if (ADDON::CAddonSystemSettings::GetInstance().GetActive( + ADDON::ScraperTypeFromContent(content), defaultScraper)) + { + scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper); + } + } + + // Set Information provider and settings + int applyto = CGUIDialogInfoProviderSettings::Show(scraper); + if (applyto >= 0) + { + bool result = false; + CVariant msgctxt; + switch (applyto) + { + case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_THISITEM: // Change information provider for specific item + result = m_musicdatabase.SetScraper(id, content, scraper); + break; + case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW: // Change information provider for the filtered items shown on this node + { + msgctxt = 38069; + if (content == CONTENT_ARTISTS) + msgctxt = 38068; + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{ 20195 }, msgctxt)) // Change information provider, confirm for all shown + { + // Set scraper for all items on current view. + std::string strPath = "musicdb://"; + if (content == CONTENT_ARTISTS) + strPath += "artists"; + else + strPath += "albums"; + URIUtils::AddSlashAtEnd(strPath); + // Items on view could be limited by navigation criteria, smart playlist rules or a filter. + // Get these options, except ID, from item path + CURL musicUrl(item->GetPath()); //Use CURL, as CMusicDbUrl removes "filter" option + if (content == CONTENT_ARTISTS) + musicUrl.RemoveOption("artistid"); + else + musicUrl.RemoveOption("albumid"); + strPath += musicUrl.GetOptions(); + result = m_musicdatabase.SetScraperAll(strPath, scraper); + } + } + break; + case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT: // Change information provider for all items + { + msgctxt = 38071; + if (content == CONTENT_ARTISTS) + msgctxt = 38070; + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, msgctxt)) // Change information provider, confirm default and clear + { + // Save scraper addon default setting values + scraper->SaveSettings(); + // Set default scraper + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (content == CONTENT_ARTISTS) + settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, scraper->ID()); + else + settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, scraper->ID()); + settings->Save(); + // Clear all item specific settings + if (content == CONTENT_ARTISTS) + result = m_musicdatabase.SetScraperAll("musicdb://artists/", nullptr); + else + result = m_musicdatabase.SetScraperAll("musicdb://albums/", nullptr); + } + } + default: + break; + } + if (!result) + return false; + + // Refresh additional information using the new settings + if (applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW || applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT) + { + // Change information provider, all artists or albums + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38072})) + OnItemInfoAll(m_vecItems->GetPath(), true); + } + else + { + // Change information provider, selected artist or album + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38073})) + { + std::string itempath = StringUtils::Format("musicdb://albums/{}/", id); + if (content == CONTENT_ARTISTS) + itempath = StringUtils::Format("musicdb://artists/{}/", id); + OnItemInfoAll(itempath, true); + } + } + } + return true; +} + +bool CGUIWindowMusicNav::OnClick(int iItem, const std::string &player /* = "" */) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) return false; + + CFileItemPtr item = m_vecItems->Get(iItem); + if (StringUtils::StartsWith(item->GetPath(), "musicsearch://")) + { + if (m_searchWithEdit) + OnSearchUpdate(); + else + { + std::string search(GetProperty("search").asString()); + CGUIKeyboardFactory::ShowAndGetFilter(search, true); + SetProperty("search", search); + } + return true; + } + if (item->IsMusicDb() && !item->m_bIsFolder) + m_musicdatabase.SetPropertiesForFileItem(*item); + + if (item->IsPlayList() && + !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders) + { + PlayItem(iItem); + return true; + } + return CGUIWindowMusicBase::OnClick(iItem, player); +} + +bool CGUIWindowMusicNav::Update(const std::string &strDirectory, bool updateFilterPath /* = true */) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + if (CGUIWindowMusicBase::Update(strDirectory, updateFilterPath)) + { + m_thumbLoader.Load(*m_unfilteredItems); + return true; + } + + return false; +} + +bool CGUIWindowMusicNav::GetDirectory(const std::string &strDirectory, CFileItemList &items) +{ + if (strDirectory.empty()) + AddSearchFolder(); + + bool bResult = CGUIWindowMusicBase::GetDirectory(strDirectory, items); + if (bResult) + { + if (items.IsPlayList()) + OnRetrieveMusicInfo(items); + } + + // update our content in the info manager + if (StringUtils::StartsWithNoCase(strDirectory, "videodb://") || items.IsVideoDb()) + { + CVideoDatabaseDirectory dir; + VIDEODATABASEDIRECTORY::NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath()); + if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS || + node == VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS) + items.SetContent("musicvideos"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE) + items.SetContent("genres"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_COUNTRY) + items.SetContent("countries"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR) + items.SetContent("artists"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_DIRECTOR) + items.SetContent("directors"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_STUDIO) + items.SetContent("studios"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_YEAR) + items.SetContent("years"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM) + items.SetContent("albums"); + else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TAGS) + items.SetContent("tags"); + else + items.SetContent(""); + } + else if (StringUtils::StartsWithNoCase(strDirectory, "musicdb://") || items.IsMusicDb()) + { + CMusicDatabaseDirectory dir; + NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath()); + if (node == NODE_TYPE_ALBUM || + node == NODE_TYPE_ALBUM_RECENTLY_ADDED || + node == NODE_TYPE_ALBUM_RECENTLY_PLAYED || + node == NODE_TYPE_ALBUM_TOP100 || + node == NODE_TYPE_DISC) // ! @todo: own content type "discs"?? + items.SetContent("albums"); + else if (node == NODE_TYPE_ARTIST) + items.SetContent("artists"); + else if (node == NODE_TYPE_SONG || + node == NODE_TYPE_SONG_TOP100 || + node == NODE_TYPE_SINGLES || + node == NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS || + node == NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS || + node == NODE_TYPE_ALBUM_TOP100_SONGS) + items.SetContent("songs"); + else if (node == NODE_TYPE_GENRE) + items.SetContent("genres"); + else if (node == NODE_TYPE_SOURCE) + items.SetContent("sources"); + else if (node == NODE_TYPE_ROLE) + items.SetContent("roles"); + else if (node == NODE_TYPE_YEAR) + items.SetContent("years"); + else + items.SetContent(""); + } + else if (items.IsPlayList()) + items.SetContent("songs"); + else if (URIUtils::PathEquals(strDirectory, "special://musicplaylists/") || + URIUtils::PathEquals(strDirectory, "library://music/playlists.xml/")) + items.SetContent("playlists"); + else if (URIUtils::PathEquals(strDirectory, "plugin://music/")) + items.SetContent("plugins"); + else if (items.IsAddonsPath()) + items.SetContent("addons"); + else if (!items.IsSourcesPath() && !items.IsVirtualDirectoryRoot() && + !items.IsLibraryFolder() && !items.IsPlugin() && !items.IsSmartPlayList()) + items.SetContent("files"); + + return bResult; +} + +void CGUIWindowMusicNav::UpdateButtons() +{ + CGUIWindowMusicBase::UpdateButtons(); + + // Update object count + int iItems = m_vecItems->Size(); + if (iItems) + { + // check for parent dir and "all" items + // should always be the first two items + for (int i = 0; i <= (iItems>=2 ? 1 : 0); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + if (pItem->IsParentFolder()) iItems--; + if (StringUtils::StartsWith(pItem->GetPath(), "/-1/")) iItems--; + } + // or the last item + if (m_vecItems->Size() > 2 && + StringUtils::StartsWith(m_vecItems->Get(m_vecItems->Size()-1)->GetPath(), "/-1/")) + iItems--; + } + std::string items = StringUtils::Format("{} {}", iItems, g_localizeStrings.Get(127)); + SET_CONTROL_LABEL(CONTROL_LABELFILES, items); + + // set the filter label + std::string strLabel; + + // "Playlists" + if (m_vecItems->IsPath("special://musicplaylists/")) + strLabel = g_localizeStrings.Get(136); + // "{Playlist Name}" + else if (m_vecItems->IsPlayList()) + { + // get playlist name from path + std::string strDummy; + URIUtils::Split(m_vecItems->GetPath(), strDummy, strLabel); + } + // everything else is from a musicdb:// path + else + { + CMusicDatabaseDirectory dir; + dir.GetLabel(m_vecItems->GetPath(), strLabel); + } + + SET_CONTROL_LABEL(CONTROL_FILTER, strLabel); + + SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled()); + + CONTROL_ENABLE_ON_CONDITION(CONTROL_UPDATE_LIBRARY, !m_vecItems->IsAddonsPath() && !m_vecItems->IsPlugin() && !m_vecItems->IsScript()); +} + +void CGUIWindowMusicNav::PlayItem(int iItem) +{ + // unlike additemtoplaylist, we need to check the items here + // before calling it since the current playlist will be stopped + // and cleared! + + // root is not allowed + if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD()) + return; + + CGUIWindowMusicBase::PlayItem(iItem); +} + +void CGUIWindowMusicNav::OnWindowLoaded() +{ + const CGUIControl *control = GetControl(CONTROL_SEARCH); + m_searchWithEdit = (control && control->GetControlType() == CGUIControl::GUICONTROL_EDIT); + + CGUIWindowMusicBase::OnWindowLoaded(); + + if (m_searchWithEdit) + { + SendMessage(GUI_MSG_SET_TYPE, CONTROL_SEARCH, CGUIEditControl::INPUT_TYPE_SEARCH); + SET_CONTROL_LABEL2(CONTROL_SEARCH, GetProperty("search").asString()); + } +} + +void CGUIWindowMusicNav::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + if (item) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // are we in the playlists location? + bool inPlaylists = m_vecItems->IsPath(CUtil::MusicPlaylistsLocation()) || + m_vecItems->IsPath("special://musicplaylists/"); + + if (m_vecItems->IsPath("sources://music/")) + { + // get the usual music shares, and anything for all media windows + CGUIDialogContextMenu::GetContextButtons("music", item, buttons); +#ifdef HAS_DVD_DRIVE + // enable Rip CD an audio disc + if (CServiceBroker::GetMediaManager().IsDiscInDrive() && item->IsCDDA()) + { + // those cds can also include Audio Tracks: CDExtra and MixedMode! + MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(); + if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1)) + { + if (CServiceBroker::GetJobManager()->IsProcessing("cdrip")) + buttons.Add(CONTEXT_BUTTON_CANCEL_RIP_CD, 14100); + else + buttons.Add(CONTEXT_BUTTON_RIP_CD, 600); + } + } +#endif + // Scan button for music sources except ".." and "Add music source" items + if (!item->IsPath("add") && !item->IsParentFolder() && + (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser)) + { + buttons.Add(CONTEXT_BUTTON_SCAN, 13352); + } + CGUIMediaWindow::GetContextButtons(itemNumber, buttons); + } + else + { + CGUIWindowMusicBase::GetContextButtons(itemNumber, buttons); + + // Scan button for real folders containing files when navigating within music sources. + // Blacklist the bespoke Kodi protocols as to many valid external protocols to whitelist + if (m_vecItems->GetContent() == "files" && // Other content not scanned to library + !inPlaylists && !m_vecItems->IsInternetStream() && // Not playlists locations or streams + !item->IsPath("add") && !item->IsParentFolder() && // Not ".." and "Add items + item->m_bIsFolder && // Folders only, but playlists can be folders too + !URIUtils::IsLibraryContent(item->GetPath()) && // database folder or .xsp files + !URIUtils::IsSpecial(item->GetPath()) && !item->IsPlugin() && !item->IsScript() && + !item->IsPlayList() && // .m3u etc. that as flagged as folders when playlistasfolders + !StringUtils::StartsWithNoCase(item->GetPath(), "addons://") && + (profileManager->GetCurrentProfile().canWriteDatabases() || + g_passwordManager.bMasterUser)) + { + buttons.Add(CONTEXT_BUTTON_SCAN, 13352); + } + + CMusicDatabaseDirectory dir; + + if (!item->IsParentFolder() && !dir.IsAllItem(item->GetPath())) + { + if (item->m_bIsFolder && !item->IsVideoDb() && + !item->IsPlugin() && !StringUtils::StartsWithNoCase(item->GetPath(), "musicsearch://")) + { + if (item->IsAlbum()) + // enable query all albums button only in album view + buttons.Add(CONTEXT_BUTTON_INFO_ALL, 20059); + else if (dir.IsArtistDir(item->GetPath())) + // enable query all artist button only in artist view + buttons.Add(CONTEXT_BUTTON_INFO_ALL, 21884); + + //Set default or clear default + NODE_TYPE nodetype = dir.GetDirectoryType(item->GetPath()); + if (!inPlaylists && + (nodetype == NODE_TYPE_ROOT || + nodetype == NODE_TYPE_OVERVIEW || + nodetype == NODE_TYPE_TOP100)) + { + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (!item->IsPath(settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW))) + buttons.Add(CONTEXT_BUTTON_SET_DEFAULT, 13335); // set default + if (!settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW).empty()) + buttons.Add(CONTEXT_BUTTON_CLEAR_DEFAULT, 13403); // clear default + } + + //Change information provider + if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums") || + StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists")) + { + // we allow the user to set information provider for albums and artists + buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20195); + } + } + if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty()) + { + CVideoDatabase database; + database.Open(); + if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString()) > -1) + buttons.Add(CONTEXT_BUTTON_GO_TO_ARTIST, 20400); + } + if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty() && + !item->GetMusicInfoTag()->GetAlbum().empty() && + !item->GetMusicInfoTag()->GetTitle().empty()) + { + CVideoDatabase database; + database.Open(); + if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle()) > -1) + buttons.Add(CONTEXT_BUTTON_PLAY_OTHER, 20401); + } + if (item->HasVideoInfoTag() && !item->m_bIsFolder) + { + if ((profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) && !item->IsPlugin()) + { + buttons.Add(CONTEXT_BUTTON_RENAME, 16105); + buttons.Add(CONTEXT_BUTTON_DELETE, 646); + } + } + if (inPlaylists && URIUtils::GetFileName(item->GetPath()) != "PartyMode.xsp" + && (item->IsPlayList() || item->IsSmartPlayList())) + buttons.Add(CONTEXT_BUTTON_DELETE, 117); + + if (!item->IsReadOnly() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("filelists.allowfiledeletion")) + { + buttons.Add(CONTEXT_BUTTON_DELETE, 117); + buttons.Add(CONTEXT_BUTTON_RENAME, 118); + } + } + } + } + // noncontextual buttons + + CGUIWindowMusicBase::GetNonContextButtons(buttons); +} + +bool CGUIWindowMusicNav::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + switch (button) + { + case CONTEXT_BUTTON_INFO: + { + if (!item->IsVideoDb()) + return CGUIWindowMusicBase::OnContextButton(itemNumber,button); + + // music videos - artists + if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/artists/")) + { + int idArtist = m_musicdatabase.GetArtistByName(item->GetLabel()); + if (idArtist == -1) + return false; + std::string path = StringUtils::Format("musicdb://artists/{}/", idArtist); + CArtist artist; + m_musicdatabase.GetArtist(idArtist, artist, false); + *item = CFileItem(artist); + item->SetPath(path); + CGUIWindowMusicBase::OnContextButton(itemNumber,button); + Refresh(); + m_viewControl.SetSelectedItem(itemNumber); + return true; + } + + // music videos - albums + if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/albums/")) + { + int idAlbum = m_musicdatabase.GetAlbumByName(item->GetLabel()); + if (idAlbum == -1) + return false; + std::string path = StringUtils::Format("musicdb://albums/{}/", idAlbum); + CAlbum album; + m_musicdatabase.GetAlbum(idAlbum, album, false); + *item = CFileItem(path,album); + item->SetPath(path); + CGUIWindowMusicBase::OnContextButton(itemNumber,button); + Refresh(); + m_viewControl.SetSelectedItem(itemNumber); + return true; + } + + if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) + { + CGUIDialogVideoInfo::ShowFor(*item); + Refresh(); + } + return true; + } + + case CONTEXT_BUTTON_INFO_ALL: + OnItemInfoAll(m_vecItems->GetPath()); + return true; + + case CONTEXT_BUTTON_SET_DEFAULT: + { + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, item->GetPath()); + settings->Save(); + return true; + } + + case CONTEXT_BUTTON_CLEAR_DEFAULT: + { + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, ""); + settings->Save(); + return true; + } + + case CONTEXT_BUTTON_GO_TO_ARTIST: + { + std::string strPath; + CVideoDatabase database; + database.Open(); + strPath = StringUtils::Format( + "videodb://musicvideos/artists/{}/", + database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString())); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath); + return true; + } + + case CONTEXT_BUTTON_PLAY_OTHER: + { + CVideoDatabase database; + database.Open(); + CVideoInfoTag details; + database.GetMusicVideoInfo("", details, database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle())); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, + static_cast<void*>(new CFileItem(details))); + return true; + } + + case CONTEXT_BUTTON_RENAME: + if (!item->IsVideoDb() && !item->IsReadOnly()) + OnRenameItem(itemNumber); + + CGUIDialogVideoInfo::UpdateVideoItemTitle(item); + CUtil::DeleteVideoDatabaseDirectoryCache(); + Refresh(); + return true; + + case CONTEXT_BUTTON_DELETE: + if (item->IsPlayList() || item->IsSmartPlayList()) + { + item->m_bIsFolder = false; + CGUIComponent *gui = CServiceBroker::GetGUI(); + if (gui && gui->ConfirmDelete(item->GetPath())) + CFileUtils::DeleteItem(item); + } + else if (!item->IsVideoDb()) + OnDeleteItem(itemNumber); + else + { + CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(item); + CUtil::DeleteVideoDatabaseDirectoryCache(); + } + Refresh(); + return true; + + case CONTEXT_BUTTON_SET_CONTENT: + return ManageInfoProvider(item); + + default: + break; + } + + return CGUIWindowMusicBase::OnContextButton(itemNumber, button); +} + +bool CGUIWindowMusicNav::GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items) +{ + std::string strParentPath=m_history.GetParentPath(); + + if (m_guiState.get() && !m_guiState->HideParentDirItems()) + { + CFileItemPtr pItem(new CFileItem("..")); + pItem->SetPath(strParentPath); + items.Add(pItem); + } + + items.SetPath(strPlayList); + CLog::Log(LOGDEBUG, "CGUIWindowMusicNav, opening playlist [{}]", strPlayList); + + std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(strPlayList)); + if (nullptr != pPlayList) + { + // load it + if (!pPlayList->Load(strPlayList)) + { + HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477}); + return false; //hmmm unable to load playlist? + } + CPlayList playlist = *pPlayList; + // convert playlist items to songs + for (int i = 0; i < playlist.size(); ++i) + { + items.Add(playlist[i]); + } + } + + return true; +} + +void CGUIWindowMusicNav::OnSearchUpdate() +{ + std::string search(CURL::Encode(GetProperty("search").asString())); + if (!search.empty()) + { + std::string path = "musicsearch://" + search + "/"; + m_history.ClearSearchHistory(); + Update(path); + } + else if (m_vecItems->IsVirtualDirectoryRoot()) + { + Update(""); + } +} + +void CGUIWindowMusicNav::FrameMove() +{ + static const int search_timeout = 2000; + // update our searching + if (m_searchTimer.IsRunning() && m_searchTimer.GetElapsedMilliseconds() > search_timeout) + { + m_searchTimer.Stop(); + OnSearchUpdate(); + } + CGUIWindowMusicBase::FrameMove(); +} + +void CGUIWindowMusicNav::AddSearchFolder() +{ + // we use a general viewstate (and not our member) here as our + // current viewstate may be specific to some other folder, and + // we know we're in the root here + CFileItemList items; + CGUIViewState* viewState = CGUIViewState::GetViewState(GetID(), items); + if (viewState) + { + // add our remove the musicsearch source + VECSOURCES &sources = viewState->GetSources(); + bool haveSearchSource = false; + bool needSearchSource = !GetProperty("search").empty() || !m_searchWithEdit; // we always need it if we don't have the edit control + for (IVECSOURCES it = sources.begin(); it != sources.end(); ++it) + { + CMediaSource& share = *it; + if (share.strPath == "musicsearch://") + { + haveSearchSource = true; + if (!needSearchSource) + { // remove it + sources.erase(it); + break; + } + } + } + if (!haveSearchSource && needSearchSource) + { + // add search share + CMediaSource share; + share.strName=g_localizeStrings.Get(137); // Search + share.strPath = "musicsearch://"; + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + sources.push_back(share); + } + m_rootDir.SetSources(sources); + delete viewState; + } +} + +std::string CGUIWindowMusicNav::GetStartFolder(const std::string &dir) +{ + std::string lower(dir); StringUtils::ToLower(lower); + if (lower == "genres") + return "musicdb://genres/"; + else if (lower == "artists") + return "musicdb://artists/"; + else if (lower == "albums") + return "musicdb://albums/"; + else if (lower == "singles") + return "musicdb://singles/"; + else if (lower == "songs") + return "musicdb://songs/"; + else if (lower == "top100") + return "musicdb://top100/"; + else if (lower == "top100songs") + return "musicdb://top100/songs/"; + else if (lower == "top100albums") + return "musicdb://top100/albums/"; + else if (lower == "recentlyaddedalbums") + return "musicdb://recentlyaddedalbums/"; + else if (lower == "recentlyplayedalbums") + return "musicdb://recentlyplayedalbums/"; + else if (lower == "compilations") + return "musicdb://compilations/"; + else if (lower == "years") + return "musicdb://years/"; + else if (lower == "files") + return "sources://music/"; + else if (lower == "boxsets") + return "musicdb://boxsets/"; + + return CGUIWindowMusicBase::GetStartFolder(dir); +} diff --git a/xbmc/music/windows/GUIWindowMusicNav.h b/xbmc/music/windows/GUIWindowMusicNav.h new file mode 100644 index 0000000..021eaad --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicNav.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "GUIWindowMusicBase.h" +#include "utils/Stopwatch.h" + +class CFileItemList; + +class CGUIWindowMusicNav : public CGUIWindowMusicBase +{ +public: + + CGUIWindowMusicNav(void); + ~CGUIWindowMusicNav(void) override; + + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void FrameMove() override; + +protected: + void OnItemLoaded(CFileItem* pItem) override {}; + // override base class methods + bool Update(const std::string &strDirectory, bool updateFilterPath = true) override; + bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override; + void UpdateButtons() override; + void PlayItem(int iItem) override; + void OnWindowLoaded() override; + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool OnClick(int iItem, const std::string &player = "") override; + std::string GetStartFolder(const std::string &url) override; + + bool GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items); + bool ManageInfoProvider(const CFileItemPtr& item); + + VECSOURCES m_shares; + + // searching + void OnSearchUpdate(); + void AddSearchFolder(); + CStopWatch m_searchTimer; ///< Timer to delay a search while more characters are entered + bool m_searchWithEdit; ///< Whether the skin supports the new edit control searching +}; diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.cpp b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp new file mode 100644 index 0000000..acc46f6 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp @@ -0,0 +1,715 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowMusicPlaylist.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "PartyModeManager.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/playercorefactory/PlayerCoreFactory.h" +#include "dialogs/GUIDialogSmartPlaylistEditor.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "music/tags/MusicInfoTag.h" +#include "playlists/PlayListM3U.h" +#include "profiles/ProfileManager.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/LabelFormatter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "view/GUIViewState.h" + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_LABELFILES 12 + +#define CONTROL_BTNSHUFFLE 20 +#define CONTROL_BTNSAVE 21 +#define CONTROL_BTNCLEAR 22 + +#define CONTROL_BTNPLAY 23 +#define CONTROL_BTNNEXT 24 +#define CONTROL_BTNPREVIOUS 25 +#define CONTROL_BTNREPEAT 26 + +CGUIWindowMusicPlayList::CGUIWindowMusicPlayList(void) + : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST, "MyPlaylist.xml") +{ + m_musicInfoLoader.SetObserver(this); + m_movingFrom = -1; +} + +CGUIWindowMusicPlayList::~CGUIWindowMusicPlayList(void) = default; + +bool CGUIWindowMusicPlayList::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_PLAYLISTPLAYER_REPEAT: + { + UpdateButtons(); + } + break; + + case GUI_MSG_PLAYLISTPLAYER_RANDOM: + case GUI_MSG_PLAYLIST_CHANGED: + { + // global playlist changed outside playlist window + UpdateButtons(); + + if (m_vecItemsUpdating) + { + CLog::Log(LOGWARNING, "CGUIWindowMusicPlayList::OnMessage - updating in progress"); + return true; + } + CUpdateGuard ug(m_vecItemsUpdating); + + Refresh(true); + + if (m_viewControl.HasControl(m_iLastControl) && m_vecItems->Size() <= 0) + { + m_iLastControl = CONTROL_BTNVIEWASICONS; + SET_CONTROL_FOCUS(m_iLastControl, 0); + } + + } + break; + + case GUI_MSG_WINDOW_DEINIT: + { + if (m_musicInfoLoader.IsLoading()) + m_musicInfoLoader.StopThread(); + + m_movingFrom = -1; + } + break; + + case GUI_MSG_WINDOW_INIT: + { + // Setup item cache for tagloader + m_musicInfoLoader.UseCacheOnHD("special://temp/archive_cache/MusicPlaylist.fi"); + + m_vecItems->SetPath("playlistmusic://"); + + // updatebuttons is called in here + if (!CGUIWindowMusicBase::OnMessage(message)) + return false; + + if (m_vecItems->Size() <= 0) + { + m_iLastControl = CONTROL_BTNVIEWASICONS; + SET_CONTROL_FOCUS(m_iLastControl, 0); + } + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingAudio() && + CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) + { + int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSong >= 0 && iSong <= m_vecItems->Size()) + m_viewControl.SetSelectedItem(iSong); + } + + return true; + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNSHUFFLE) + { + if (!g_partyModeManager.IsEnabled()) + { + CServiceBroker::GetPlaylistPlayer().SetShuffle( + PLAYLIST::TYPE_MUSIC, + !(CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC))); + CMediaSettings::GetInstance().SetMusicPlaylistShuffled( + CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC)); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + UpdateButtons(); + Refresh(); + } + } + else if (iControl == CONTROL_BTNSAVE) + { + if (m_musicInfoLoader.IsLoading()) // needed since we destroy m_vecitems to save memory + m_musicInfoLoader.StopThread(); + + SavePlayList(); + } + else if (iControl == CONTROL_BTNCLEAR) + { + if (m_musicInfoLoader.IsLoading()) + m_musicInfoLoader.StopThread(); + + ClearPlayList(); + } + else if (iControl == CONTROL_BTNPLAY) + { + m_guiState->SetPlaylistDirectory("playlistmusic://"); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC); + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().Play(m_viewControl.GetSelectedItem(), ""); + UpdateButtons(); + } + else if (iControl == CONTROL_BTNNEXT) + { + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC); + CServiceBroker::GetPlaylistPlayer().PlayNext(); + } + else if (iControl == CONTROL_BTNPREVIOUS) + { + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC); + CServiceBroker::GetPlaylistPlayer().PlayPrevious(); + } + else if (iControl == CONTROL_BTNREPEAT) + { + // increment repeat state + PLAYLIST::RepeatState state = + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC); + if (state == PLAYLIST::RepeatState::NONE) + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC, + PLAYLIST::RepeatState::ALL); + else if (state == PLAYLIST::RepeatState::ALL) + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC, + PLAYLIST::RepeatState::ONE); + else + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC, + PLAYLIST::RepeatState::NONE); + + // save settings + CMediaSettings::GetInstance().SetMusicPlaylistRepeat( + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC) == + PLAYLIST::RepeatState::ALL); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + UpdateButtons(); + } + else if (m_viewControl.HasControl(iControl)) + { + int iAction = message.GetParam1(); + int iItem = m_viewControl.GetSelectedItem(); + if (iAction == ACTION_DELETE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK) + { + RemovePlayListItem(iItem); + MarkPlaying(); + } + } + } + break; + + } + return CGUIWindowMusicBase::OnMessage(message); +} + +bool CGUIWindowMusicPlayList::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_PARENT_DIR) + { + // Playlist has no parent dirs + return true; + } + if (action.GetID() == ACTION_SHOW_PLAYLIST) + { + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + } + if ((action.GetID() == ACTION_MOVE_ITEM_UP) || (action.GetID() == ACTION_MOVE_ITEM_DOWN)) + { + int iItem = -1; + int iFocusedControl = GetFocusedControlID(); + if (m_viewControl.HasControl(iFocusedControl)) + iItem = m_viewControl.GetSelectedItem(); + OnMove(iItem, action.GetID()); + return true; + } + return CGUIWindowMusicBase::OnAction(action); +} + +bool CGUIWindowMusicPlayList::OnBack(int actionID) +{ + CancelUpdateItems(); + + if (actionID == ACTION_NAV_BACK) + return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up + return CGUIWindowMusicBase::OnBack(actionID); +} + +bool CGUIWindowMusicPlayList::MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate /* = true */) +{ + int iSelected = iItem; + int iNew = iSelected; + if (iAction == ACTION_MOVE_ITEM_UP) + iNew--; + else + iNew++; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + // is the currently playing item affected? + bool bFixCurrentSong = false; + if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) && + appPlayer->IsPlayingAudio() && + ((CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iSelected) || + (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iNew))) + bFixCurrentSong = true; + + PLAYLIST::CPlayList& playlist = + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC); + if (playlist.Swap(iSelected, iNew)) + { + // Correct the current playing song in playlistplayer + if (bFixCurrentSong) + { + int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSelected == iCurrentSong) + iCurrentSong = iNew; + else if (iNew == iCurrentSong) + iCurrentSong = iSelected; + CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong); + } + + if (bUpdate) + Refresh(); + return true; + } + + return false; +} + +void CGUIWindowMusicPlayList::SavePlayList() +{ + std::string strNewFileName; + if (CGUIKeyboardFactory::ShowAndGetInput(strNewFileName, CVariant{g_localizeStrings.Get(16012)}, false)) + { + // need 2 rename it + strNewFileName = CUtil::MakeLegalFileName(strNewFileName); + strNewFileName += ".m3u"; + std::string strPath = URIUtils::AddFileToFolder( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH), + "music", + strNewFileName); + + // get selected item + int iItem = m_viewControl.GetSelectedItem(); + std::string strSelectedItem = ""; + if (iItem >= 0 && iItem < m_vecItems->Size()) + { + CFileItemPtr pItem = m_vecItems->Get(iItem); + if (!pItem->IsParentFolder()) + { + GetDirectoryHistoryString(pItem.get(), strSelectedItem); + } + } + + std::string strOldDirectory = m_vecItems->GetPath(); + m_history.SetSelectedItem(strSelectedItem, strOldDirectory); + + PLAYLIST::CPlayListM3U playlist; + for (int i = 0; i < m_vecItems->Size(); ++i) + { + CFileItemPtr pItem = m_vecItems->Get(i); + + // Musicdatabase items should contain the real path instead of a musicdb url + // otherwise the user can't save and reuse the playlist when the musicdb gets deleted + if (pItem->IsMusicDb()) + pItem->SetPath(pItem->GetMusicInfoTag()->GetURL()); + + playlist.Add(pItem); + } + CLog::Log(LOGDEBUG, "Saving music playlist: [{}]", strPath); + playlist.Save(strPath); + Refresh(); // need to update + } +} + +void CGUIWindowMusicPlayList::ClearPlayList() +{ + ClearFileItems(); + CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC); + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) + { + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE); + } + Refresh(); + SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0); +} + +void CGUIWindowMusicPlayList::RemovePlayListItem(int iItem) +{ + if (iItem < 0 || iItem > m_vecItems->Size()) return; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + // The current playing song can't be removed + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC && + appPlayer->IsPlayingAudio() && CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iItem) + return ; + + CServiceBroker::GetPlaylistPlayer().Remove(PLAYLIST::TYPE_MUSIC, iItem); + + Refresh(); + + if (m_vecItems->Size() <= 0) + { + SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0); + } + else + { + m_viewControl.SetSelectedItem(iItem); + } + + g_partyModeManager.OnSongChange(); +} + +void CGUIWindowMusicPlayList::UpdateButtons() +{ + CGUIWindowMusicBase::UpdateButtons(); + + // Update playlist buttons + if (m_vecItems->Size() && !g_partyModeManager.IsEnabled()) + { + CONTROL_ENABLE(CONTROL_BTNSHUFFLE); + CONTROL_ENABLE(CONTROL_BTNSAVE); + CONTROL_ENABLE(CONTROL_BTNCLEAR); + CONTROL_ENABLE(CONTROL_BTNREPEAT); + CONTROL_ENABLE(CONTROL_BTNPLAY); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingAudio() && + CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) + { + CONTROL_ENABLE(CONTROL_BTNNEXT); + CONTROL_ENABLE(CONTROL_BTNPREVIOUS); + } + else + { + CONTROL_DISABLE(CONTROL_BTNNEXT); + CONTROL_DISABLE(CONTROL_BTNPREVIOUS); + } + } + else + { + // disable buttons if party mode is enabled too + CONTROL_DISABLE(CONTROL_BTNSHUFFLE); + CONTROL_DISABLE(CONTROL_BTNSAVE); + CONTROL_DISABLE(CONTROL_BTNCLEAR); + CONTROL_DISABLE(CONTROL_BTNREPEAT); + CONTROL_DISABLE(CONTROL_BTNPLAY); + CONTROL_DISABLE(CONTROL_BTNNEXT); + CONTROL_DISABLE(CONTROL_BTNPREVIOUS); + } + + // update buttons + CONTROL_DESELECT(CONTROL_BTNSHUFFLE); + if (CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC)) + CONTROL_SELECT(CONTROL_BTNSHUFFLE); + + // update repeat button + int iLocalizedString; + PLAYLIST::RepeatState repState = + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC); + if (repState == PLAYLIST::RepeatState::NONE) + iLocalizedString = 595; // Repeat: Off + else if (repState == PLAYLIST::RepeatState::ONE) + iLocalizedString = 596; // Repeat: One + else + iLocalizedString = 597; // Repeat: All + + SET_CONTROL_LABEL(CONTROL_BTNREPEAT, g_localizeStrings.Get(iLocalizedString)); + + // Update object count label + std::string items = + StringUtils::Format("{} {}", m_vecItems->GetObjectCount(), g_localizeStrings.Get(127)); + SET_CONTROL_LABEL(CONTROL_LABELFILES, items); + + MarkPlaying(); +} + +bool CGUIWindowMusicPlayList::OnPlayMedia(int iItem, const std::string &player) +{ + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Play(iItem); + else + { + PLAYLIST::Id playlistId = m_guiState->GetPlaylist(); + if (playlistId != PLAYLIST::TYPE_NONE) + { + if (m_guiState) + m_guiState->SetPlaylistDirectory(m_vecItems->GetPath()); + + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId); + CServiceBroker::GetPlaylistPlayer().Play(iItem, player); + } + else + { + // Reset Playlistplayer, playback started now does + // not use the playlistplayer. + CFileItemPtr pItem=m_vecItems->Get(iItem); + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE); + g_application.PlayFile(*pItem, player); + } + } + + return true; +} + +void CGUIWindowMusicPlayList::OnItemLoaded(CFileItem* pItem) +{ + if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->Loaded()) + { // set label 1+2 from tags + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT); + if (strTrack.empty()) + strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + CLabelFormatter formatter(strTrack, "%D"); + formatter.FormatLabels(pItem); + } // if (pItem->m_musicInfoTag.Loaded()) + else + { + // Our tag may have a duration even if its not loaded + if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetDuration()) + { + int nDuration = pItem->GetMusicInfoTag()->GetDuration(); + if (nDuration > 0) + pItem->SetLabel2(StringUtils::SecondsToTimeString(nDuration)); + } + else if (pItem->GetLabel() == "") // pls labels come in preformatted + { + // FIXME: get the position of the item in the playlist + // currently it is hacked into m_iprogramCount + + // No music info and it's not CDDA so we'll just show the filename + std::string str; + str = CUtil::GetTitleFromPath(pItem->GetPath()); + str = StringUtils::Format("{:02}. {} ", pItem->m_iprogramCount, str); + pItem->SetLabel(str); + } + } +} + +bool CGUIWindowMusicPlayList::Update(const std::string& strDirectory, bool updateFilterPath /* = true */) +{ + if (m_musicInfoLoader.IsLoading()) + m_musicInfoLoader.StopThread(); + + if (!CGUIWindowMusicBase::Update(strDirectory, updateFilterPath)) + return false; + + if (m_vecItems->GetContent().empty()) + m_vecItems->SetContent("songs"); + + m_musicInfoLoader.Load(*m_vecItems); + return true; +} + +void CGUIWindowMusicPlayList::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + // is this playlist playing? + int itemPlaying = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + { + CFileItemPtr item; + item = m_vecItems->Get(itemNumber); + + if (m_movingFrom >= 0) + { + // we can move the item to any position not where we are, and any position not above currently + // playing item in party mode + if (itemNumber != m_movingFrom && (!g_partyModeManager.IsEnabled() || itemNumber > itemPlaying)) + buttons.Add(CONTEXT_BUTTON_MOVE_HERE, 13252); // move item here + buttons.Add(CONTEXT_BUTTON_CANCEL_MOVE, 13253); + } + else + { + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + // aren't in a move + // check what players we have, if we have multiple display play with option + std::vector<std::string> players; + playerCoreFactory.GetPlayers(*item, players); + if (players.size() > 1) + buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With... + + if (itemNumber > (g_partyModeManager.IsEnabled() ? 1 : 0)) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332); + if (itemNumber + 1 < m_vecItems->Size()) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333); + if (!g_partyModeManager.IsEnabled() || itemNumber != itemPlaying) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM, 13251); + if (itemNumber != itemPlaying) + buttons.Add(CONTEXT_BUTTON_DELETE, 1210); // Remove + } + } + + if (g_partyModeManager.IsEnabled()) + { + buttons.Add(CONTEXT_BUTTON_EDIT_PARTYMODE, 21439); + buttons.Add(CONTEXT_BUTTON_CANCEL_PARTYMODE, 588); // cancel party mode + } +} + +bool CGUIWindowMusicPlayList::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + switch (button) + { + case CONTEXT_BUTTON_PLAY_WITH: + { + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + if (!item) + break; + + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + std::vector<std::string> players; + playerCoreFactory.GetPlayers(*item, players); + std::string player = playerCoreFactory.SelectPlayerDialog(players); + if (!player.empty()) + OnClick(itemNumber, player); + return true; + } + case CONTEXT_BUTTON_MOVE_ITEM: + m_movingFrom = itemNumber; + return true; + + case CONTEXT_BUTTON_MOVE_HERE: + MoveItem(m_movingFrom, itemNumber); + m_movingFrom = -1; + return true; + + case CONTEXT_BUTTON_CANCEL_MOVE: + m_movingFrom = -1; + return true; + + case CONTEXT_BUTTON_MOVE_ITEM_UP: + OnMove(itemNumber, ACTION_MOVE_ITEM_UP); + return true; + + case CONTEXT_BUTTON_MOVE_ITEM_DOWN: + OnMove(itemNumber, ACTION_MOVE_ITEM_DOWN); + return true; + + case CONTEXT_BUTTON_DELETE: + RemovePlayListItem(itemNumber); + return true; + + case CONTEXT_BUTTON_CANCEL_PARTYMODE: + g_partyModeManager.Disable(); + return true; + + case CONTEXT_BUTTON_EDIT_PARTYMODE: + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + std::string playlist = profileManager->GetUserDataItem("PartyMode.xsp"); + if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist)) + { + // apply new rules + g_partyModeManager.Disable(); + g_partyModeManager.Enable(); + } + return true; + } + + default: + break; + } + return CGUIWindowMusicBase::OnContextButton(itemNumber, button); +} + + +void CGUIWindowMusicPlayList::OnMove(int iItem, int iAction) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) return; + + bool bRestart = m_musicInfoLoader.IsLoading(); + if (bRestart) + m_musicInfoLoader.StopThread(); + + MoveCurrentPlayListItem(iItem, iAction); + + if (bRestart) + m_musicInfoLoader.Load(*m_vecItems); +} + +void CGUIWindowMusicPlayList::MoveItem(int iStart, int iDest) +{ + if (iStart < 0 || iStart >= m_vecItems->Size()) return; + if (iDest < 0 || iDest >= m_vecItems->Size()) return; + + // default to move up + int iAction = ACTION_MOVE_ITEM_UP; + int iDirection = -1; + // are we moving down? + if (iStart < iDest) + { + iAction = ACTION_MOVE_ITEM_DOWN; + iDirection = 1; + } + + bool bRestart = m_musicInfoLoader.IsLoading(); + if (bRestart) + m_musicInfoLoader.StopThread(); + + // keep swapping until you get to the destination or you + // hit the currently playing song + int i = iStart; + while (i != iDest) + { + // try to swap adjacent items + if (MoveCurrentPlayListItem(i, iAction, false)) + i = i + (1 * iDirection); + // we hit currently playing song, so abort + else + break; + } + Refresh(); + + if (bRestart) + m_musicInfoLoader.Load(*m_vecItems); +} + +void CGUIWindowMusicPlayList::MarkPlaying() +{ + /* // clear markings + for (int i = 0; i < m_vecItems->Size(); i++) + m_vecItems->Get(i)->Select(false); + + // mark the currently playing item + if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == TYPE_MUSIC) && (g_application.GetAppPlayer().IsPlayingAudio())) + { + int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSong >= 0 && iSong <= m_vecItems->Size()) + m_vecItems->Get(iSong)->Select(true); + }*/ +} + diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.h b/xbmc/music/windows/GUIWindowMusicPlaylist.h new file mode 100644 index 0000000..019a1c7 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicPlaylist.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "GUIWindowMusicBase.h" + +class CGUIWindowMusicPlayList : public CGUIWindowMusicBase +{ +public: + CGUIWindowMusicPlayList(void); + ~CGUIWindowMusicPlayList(void) override; + + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + bool OnBack(int actionID) override; + + void RemovePlayListItem(int iItem); + void MoveItem(int iStart, int iDest); + +protected: + bool GoParentFolder() override { return false; } + void UpdateButtons() override; + void OnItemLoaded(CFileItem* pItem) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + void OnMove(int iItem, int iAction); + bool OnPlayMedia(int iItem, const std::string &player = "") override; + + void SavePlayList(); + void ClearPlayList(); + void MarkPlaying(); + + bool MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate = true); + + int m_movingFrom; + VECSOURCES m_shares; +}; diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp new file mode 100644 index 0000000..393923d --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowMusicPlaylistEditor.h" + +#include "Autorun.h" +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "filesystem/PlaylistFileDirectory.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "music/MusicUtils.h" +#include "playlists/PlayListM3U.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" + +#define CONTROL_LABELFILES 12 + +#define CONTROL_LOAD_PLAYLIST 6 +#define CONTROL_SAVE_PLAYLIST 7 +#define CONTROL_CLEAR_PLAYLIST 8 + +#define CONTROL_LIST 50 +#define CONTROL_PLAYLIST 100 +#define CONTROL_LABEL_PLAYLIST 101 + +CGUIWindowMusicPlaylistEditor::CGUIWindowMusicPlaylistEditor(void) + : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST_EDITOR, "MyMusicPlaylistEditor.xml") +{ + m_playlistThumbLoader.SetObserver(this); + m_playlist = new CFileItemList; +} + +CGUIWindowMusicPlaylistEditor::~CGUIWindowMusicPlaylistEditor(void) +{ + delete m_playlist; +} + +bool CGUIWindowMusicPlaylistEditor::OnBack(int actionID) +{ + if (actionID == ACTION_NAV_BACK && !m_viewControl.HasControl(GetFocusedControlID())) + return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up + return CGUIWindowMusicBase::OnBack(actionID); +} + +bool CGUIWindowMusicPlaylistEditor::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_CONTEXT_MENU) + { + int iControl = GetFocusedControlID(); + if (iControl == CONTROL_PLAYLIST) + { + OnPlaylistContext(); + return true; + } + else if (iControl == CONTROL_LIST) + { + OnSourcesContext(); + return true; + } + } + return CGUIWindow::OnAction(action); +} + +bool CGUIWindowMusicPlaylistEditor::OnClick(int iItem, const std::string& player /* = "" */) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) return false; + CFileItemPtr item = m_vecItems->Get(iItem); + + // Expand .m3u files in sources list when clicked on regardless of <playlistasfolders> + if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE)) + return Update(item->GetPath()); + // Avoid playback (default click behaviour) of media files + if (!item->m_bIsFolder) + return false; + + return CGUIWindowMusicBase::OnClick(iItem, player); +} + +bool CGUIWindowMusicPlaylistEditor::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + if (m_playlistThumbLoader.IsLoading()) + m_playlistThumbLoader.StopThread(); + CGUIWindowMusicBase::OnMessage(message); + return true; + + case GUI_MSG_WINDOW_INIT: + { + if (m_vecItems->GetPath() == "?") + m_vecItems->SetPath(""); + CGUIWindowMusicBase::OnMessage(message); + + if (message.GetNumStringParams()) + LoadPlaylist(message.GetStringParam()); + + return true; + } + break; + + case GUI_MSG_NOTIFY_ALL: + { + if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA) + DeleteRemoveableMediaDirectoryCache(); + } + break; + + case GUI_MSG_CLICKED: + { + int control = message.GetSenderId(); + if (control == CONTROL_PLAYLIST) + { + int item = GetCurrentPlaylistItem(); + int action = message.GetParam1(); + if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK) + OnPlaylistContext(); + else if (action == ACTION_QUEUE_ITEM || action == ACTION_DELETE_ITEM || + action == ACTION_MOUSE_MIDDLE_CLICK) + OnDeletePlaylistItem(item); + else if (action == ACTION_MOVE_ITEM_UP) + OnMovePlaylistItem(item, -1); + else if (action == ACTION_MOVE_ITEM_DOWN) + OnMovePlaylistItem(item, 1); + return true; + } + else if (control == CONTROL_LIST) + { + int action = message.GetParam1(); + if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK) + { + OnSourcesContext(); + return true; + } + } + else if (control == CONTROL_LOAD_PLAYLIST) + { // load a playlist + OnLoadPlaylist(); + return true; + } + else if (control == CONTROL_SAVE_PLAYLIST) + { // save the playlist + OnSavePlaylist(); + return true; + } + else if (control == CONTROL_CLEAR_PLAYLIST) + { // clear the playlist + ClearPlaylist(); + return true; + } + } + break; + } + + return CGUIWindowMusicBase::OnMessage(message); +} + +bool CGUIWindowMusicPlaylistEditor::GetDirectory(const std::string &strDirectory, CFileItemList &items) +{ + items.Clear(); + if (strDirectory.empty()) + { // root listing - list files:// and musicdb:// + CFileItemPtr files(new CFileItem("sources://music/", true)); + files->SetLabel(g_localizeStrings.Get(744)); + files->SetLabelPreformatted(true); + files->m_bIsShareOrDrive = true; + items.Add(files); + + CFileItemPtr mdb(new CFileItem("library://music/", true)); + mdb->SetLabel(g_localizeStrings.Get(14022)); + mdb->SetLabelPreformatted(true); + mdb->m_bIsShareOrDrive = true; + items.SetPath(""); + items.Add(mdb); + + CFileItemPtr vdb(new CFileItem("videodb://musicvideos/", true)); + vdb->SetLabel(g_localizeStrings.Get(20389)); + vdb->SetLabelPreformatted(true); + vdb->m_bIsShareOrDrive = true; + items.SetPath(""); + items.Add(vdb); + + return true; + } + + if (!CGUIWindowMusicBase::GetDirectory(strDirectory, items)) + return false; + + // check for .CUE files here. + items.FilterCueItems(); + + return true; +} + +void CGUIWindowMusicPlaylistEditor::OnPrepareFileItems(CFileItemList &items) +{ + CGUIWindowMusicBase::OnPrepareFileItems(items); + + RetrieveMusicInfo(); +} + +void CGUIWindowMusicPlaylistEditor::UpdateButtons() +{ + CGUIWindowMusicBase::UpdateButtons(); + + // Update object count label + std::string items = StringUtils::Format("{} {}", m_vecItems->GetObjectCount(), + g_localizeStrings.Get(127)); // " 14 Objects" + SET_CONTROL_LABEL(CONTROL_LABELFILES, items); +} + +void CGUIWindowMusicPlaylistEditor::DeleteRemoveableMediaDirectoryCache() +{ + CUtil::DeleteDirectoryCache("r-"); +} + +void CGUIWindowMusicPlaylistEditor::PlayItem(int iItem) +{ + // unlike additemtoplaylist, we need to check the items here + // before calling it since the current playlist will be stopped + // and cleared! + + // we're at the root source listing + if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD()) + return; + +#ifdef HAS_DVD_DRIVE + if (m_vecItems->Get(iItem)->IsDVD()) + MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath()); + else +#endif + CGUIWindowMusicBase::PlayItem(iItem); +} + +void CGUIWindowMusicPlaylistEditor::OnQueueItem(int iItem, bool) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) + return; + + // add this item to our playlist. We make a new copy here as we may be rendering them side by side, + // and thus want a different layout for each item + CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem))); + CFileItemList newItems; + MUSIC_UTILS::GetItemsForPlayList(item, newItems); + AppendToPlaylist(newItems); +} + +bool CGUIWindowMusicPlaylistEditor::Update(const std::string &strDirectory, bool updateFilterPath /* = true */) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath)) + return false; + + m_vecItems->SetContent("files"); + m_thumbLoader.Load(*m_vecItems); + + // update our playlist control + UpdatePlaylist(); + return true; +} + +void CGUIWindowMusicPlaylistEditor::ClearPlaylist() +{ + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PLAYLIST); + OnMessage(msg); + + m_playlist->Clear(); +} + +void CGUIWindowMusicPlaylistEditor::UpdatePlaylist() +{ + if (m_playlistThumbLoader.IsLoading()) + m_playlistThumbLoader.StopThread(); + + // deselect all items + for (int i = 0; i < m_playlist->Size(); i++) + m_playlist->Get(i)->Select(false); + + // bind them to the list + CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PLAYLIST, 0, 0, m_playlist); + OnMessage(msg); + + // indicate how many songs we have + std::string items = StringUtils::Format("{} {}", m_playlist->Size(), + g_localizeStrings.Get(134)); // "123 Songs" + SET_CONTROL_LABEL(CONTROL_LABEL_PLAYLIST, items); + + m_playlistThumbLoader.Load(*m_playlist); +} + +int CGUIWindowMusicPlaylistEditor::GetCurrentPlaylistItem() +{ + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PLAYLIST); + OnMessage(msg); + int item = msg.GetParam1(); + if (item > m_playlist->Size()) + return -1; + return item; +} + +void CGUIWindowMusicPlaylistEditor::OnDeletePlaylistItem(int item) +{ + if (item < 0) return; + m_playlist->Remove(item); + UpdatePlaylist(); + // select the next item + CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item); + OnMessage(msg); +} + +void CGUIWindowMusicPlaylistEditor::OnMovePlaylistItem(int item, int direction) +{ + if (item < 0) return; + if (item + direction >= m_playlist->Size() || item + direction < 0) + return; + m_playlist->Swap(item, item + direction); + UpdatePlaylist(); + CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item + direction); + OnMessage(msg); +} + +void CGUIWindowMusicPlaylistEditor::OnLoadPlaylist() +{ + // Prompt user for file to load from music playlists folder + std::string playlist; + if (CGUIDialogFileBrowser::ShowAndGetFile("special://musicplaylists/", + ".m3u|.pls|.b4s|.wpl|.xspf", g_localizeStrings.Get(656), + playlist)) + LoadPlaylist(playlist); +} + +void CGUIWindowMusicPlaylistEditor::LoadPlaylist(const std::string &playlist) +{ + const CURL pathToUrl(playlist); + if (pathToUrl.IsProtocol("newplaylist")) + { + ClearPlaylist(); + m_strLoadedPlaylist.clear(); + return; + } + + XFILE::CPlaylistFileDirectory dir; + CFileItemList items; + if (dir.GetDirectory(pathToUrl, items)) + { + ClearPlaylist(); + AppendToPlaylist(items); + m_strLoadedPlaylist = playlist; + } +} + +void CGUIWindowMusicPlaylistEditor::OnSavePlaylist() +{ + // saves playlist to the playlist folder + std::string name = URIUtils::GetFileName(m_strLoadedPlaylist); + URIUtils::RemoveExtension(name); + + if (CGUIKeyboardFactory::ShowAndGetInput(name, CVariant{g_localizeStrings.Get(16012)}, false)) + { // save playlist as an .m3u + PLAYLIST::CPlayListM3U playlist; + playlist.Add(*m_playlist); + std::string path = URIUtils::AddFileToFolder( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH), + "music", + name + ".m3u"); + + playlist.Save(path); + m_strLoadedPlaylist = name; + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, + g_localizeStrings.Get(559), // "Playlist" + g_localizeStrings.Get(35259)); // "Saved" + } +} + +void CGUIWindowMusicPlaylistEditor::AppendToPlaylist(CFileItemList &newItems) +{ + OnRetrieveMusicInfo(newItems); + FormatItemLabels(newItems, LABEL_MASKS(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT), "%D", "%L", "")); + m_playlist->Append(newItems); + UpdatePlaylist(); +} + +void CGUIWindowMusicPlaylistEditor::OnSourcesContext() +{ + CFileItemPtr item = GetCurrentListItem(); + CContextButtons buttons; + if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE)) + buttons.Add(CONTEXT_BUTTON_BROWSE_INTO, 37015); //Browse into + if (item && !item->IsParentFolder() && !m_vecItems->IsVirtualDirectoryRoot()) + buttons.Add(CONTEXT_BUTTON_QUEUE_ITEM, 15019); // Add (to playlist) + + int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons); + if (btnid == CONTEXT_BUTTON_QUEUE_ITEM) + OnQueueItem(m_viewControl.GetSelectedItem(), false); + else if (btnid == CONTEXT_BUTTON_BROWSE_INTO) + Update(item->GetPath()); +} + +void CGUIWindowMusicPlaylistEditor::OnPlaylistContext() +{ + int item = GetCurrentPlaylistItem(); + CContextButtons buttons; + if (item > 0) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332); + if (item >= 0 && item < m_playlist->Size() - 1) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333); + if (item >= 0) + buttons.Add(CONTEXT_BUTTON_DELETE, 1210); + + int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons); + if (btnid == CONTEXT_BUTTON_MOVE_ITEM_UP) + OnMovePlaylistItem(item, -1); + else if (btnid == CONTEXT_BUTTON_MOVE_ITEM_DOWN) + OnMovePlaylistItem(item, 1); + else if (btnid == CONTEXT_BUTTON_DELETE) + OnDeletePlaylistItem(item); +} + +bool CGUIWindowMusicPlaylistEditor::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + switch (button) + { + case CONTEXT_BUTTON_QUEUE_ITEM: + OnQueueItem(itemNumber); + return true; + + default: + break; + } + + return CGUIWindowMusicBase::OnContextButton(itemNumber, button); +} diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h new file mode 100644 index 0000000..3859b55 --- /dev/null +++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "GUIWindowMusicBase.h" + +class CFileItemList; + +class CGUIWindowMusicPlaylistEditor : public CGUIWindowMusicBase +{ +public: + CGUIWindowMusicPlaylistEditor(void); + ~CGUIWindowMusicPlaylistEditor(void) override; + + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + bool OnClick(int iItem, const std::string &player = "") override; + bool OnBack(int actionID) override; + +protected: + bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override; + void UpdateButtons() override; + bool Update(const std::string &strDirectory, bool updateFilterPath = true) override; + void OnPrepareFileItems(CFileItemList &items) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + void OnQueueItem(int iItem, bool first = false) override; + std::string GetStartFolder(const std::string& dir) override { return ""; } + + void OnSourcesContext(); + void OnPlaylistContext(); + int GetCurrentPlaylistItem(); + void OnDeletePlaylistItem(int item); + void UpdatePlaylist(); + void ClearPlaylist(); + void OnSavePlaylist(); + void OnLoadPlaylist(); + void AppendToPlaylist(CFileItemList &newItems); + void OnMovePlaylistItem(int item, int direction); + + void LoadPlaylist(const std::string &playlist); + + // new method + void PlayItem(int iItem) override; + + void DeleteRemoveableMediaDirectoryCache(); + + CMusicThumbLoader m_playlistThumbLoader; + + CFileItemList* m_playlist; + std::string m_strLoadedPlaylist; +}; diff --git a/xbmc/music/windows/GUIWindowVisualisation.cpp b/xbmc/music/windows/GUIWindowVisualisation.cpp new file mode 100644 index 0000000..805c2df --- /dev/null +++ b/xbmc/music/windows/GUIWindowVisualisation.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowVisualisation.h" + +#include "GUIInfoManager.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIDialog.h" +#include "guilib/GUIWindowManager.h" +#include "input/Key.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace MUSIC_INFO; + +#define START_FADE_LENGTH 2.0f // 2 seconds on startup + +#define CONTROL_VIS 2 + +CGUIWindowVisualisation::CGUIWindowVisualisation(void) + : CGUIWindow(WINDOW_VISUALISATION, "MusicVisualisation.xml") +{ + m_bShowPreset = false; + m_loadType = KEEP_IN_MEMORY; +} + +bool CGUIWindowVisualisation::OnAction(const CAction &action) +{ + bool passToVis = false; + switch (action.GetID()) + { + case ACTION_VIS_PRESET_NEXT: + case ACTION_VIS_PRESET_PREV: + case ACTION_VIS_PRESET_RANDOM: + case ACTION_VIS_RATE_PRESET_PLUS: + case ACTION_VIS_RATE_PRESET_MINUS: + passToVis = true; + break; + + case ACTION_SHOW_INFO: + { + m_initTimer.Stop(); + CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS, + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().ToggleShowInfo()); + return true; + } + break; + + case ACTION_SHOW_OSD: + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_MUSIC_OSD); + return true; + + case ACTION_SHOW_GUI: + // save the settings + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + break; + + case ACTION_VIS_PRESET_LOCK: + { // show the locked icon + fall through so that the vis handles the locking + if (!m_bShowPreset) + { + m_lockedTimer.StartZero(); + } + passToVis = true; + } + break; + case ACTION_VIS_PRESET_SHOW: + { + if (!m_lockedTimer.IsRunning() || m_bShowPreset) + m_bShowPreset = !m_bShowPreset; + return true; + } + break; + + case ACTION_DECREASE_RATING: + case ACTION_INCREASE_RATING: + { + // actual action is taken care of in CApplication::OnAction() + m_initTimer.StartZero(); + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true); + } + break; + //! @todo These should be mapped to its own function - at the moment it's overriding + //! the global action of fastforward/rewind and OSD. +/* case KEY_BUTTON_Y: + g_application.m_CdgParser.Pause(); + return true; + break; + + case ACTION_ANALOG_FORWARD: + // calculate the speed based on the amount the button is held down + if (action.GetAmount()) + { + float AVDelay = g_application.m_CdgParser.GetAVDelay(); + g_application.m_CdgParser.SetAVDelay(AVDelay - action.GetAmount() / 4.0f); + return true; + } + break;*/ + } + + if (passToVis) + { + CGUIControl *control = GetControl(CONTROL_VIS); + if (control) + return control->OnAction(action); + } + + return CGUIWindow::OnAction(action); +} + +bool CGUIWindowVisualisation::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_GET_VISUALISATION: + case GUI_MSG_VISUALISATION_RELOAD: + case GUI_MSG_PLAYBACK_STARTED: + { + CGUIControl *control = GetControl(CONTROL_VIS); + if (control) + return control->OnMessage(message); + } + break; + case GUI_MSG_VISUALISATION_ACTION: + { + CAction action(message.GetParam1()); + return OnAction(action); + } + case GUI_MSG_WINDOW_DEINIT: + { + if (IsActive()) // save any changed settings from the OSD + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + // close all active modal dialogs + CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true); + } + break; + case GUI_MSG_WINDOW_INIT: + { + // check whether we've come back here from a window during which time we've actually + // stopped playing music + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (message.GetParam1() == WINDOW_INVALID && !appPlayer->IsPlayingAudio()) + { // why are we here if nothing is playing??? + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + } + + // hide or show the preset button(s) + CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager(); + infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true); // always show the info initially. + CGUIWindow::OnMessage(message); + if (infoMgr.GetCurrentSongTag()) + m_tag = *infoMgr.GetCurrentSongTag(); + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS)) + { // always on + m_initTimer.Stop(); + } + else + { + // start display init timer (fade out after 3 secs...) + m_initTimer.StartZero(); + } + return true; + } + } + return CGUIWindow::OnMessage(message); +} + +EVENT_RESULT CGUIWindowVisualisation::OnMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + if (event.m_id == ACTION_MOUSE_RIGHT_CLICK) + { // no control found to absorb this click - go back to GUI + OnAction(CAction(ACTION_SHOW_GUI)); + return EVENT_RESULT_HANDLED; + } + if (event.m_id == ACTION_GESTURE_NOTIFY) + return EVENT_RESULT_UNHANDLED; + if (event.m_id != ACTION_MOUSE_MOVE || event.m_offsetX || event.m_offsetY) + { // some other mouse action has occurred - bring up the OSD + CGUIDialog *pOSD = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_MUSIC_OSD); + if (pOSD) + { + pOSD->SetAutoClose(3000); + pOSD->Open(); + } + return EVENT_RESULT_HANDLED; + } + return EVENT_RESULT_UNHANDLED; +} + +void CGUIWindowVisualisation::FrameMove() +{ + CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager(); + + // check for a tag change + const CMusicInfoTag* tag = infoMgr.GetCurrentSongTag(); + if (tag && *tag != m_tag) + { // need to fade in then out again + m_tag = *tag; + // fade in + m_initTimer.StartZero(); + infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true); + } + if (m_initTimer.IsRunning() && m_initTimer.GetElapsedSeconds() > (float)CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_songInfoDuration) + { + m_initTimer.Stop(); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS)) + { // reached end of fade in, fade out again + infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(false); + } + } + // show or hide the locked texture + if (m_lockedTimer.IsRunning() && m_lockedTimer.GetElapsedSeconds() > START_FADE_LENGTH) + { + m_lockedTimer.Stop(); + } + CGUIWindow::FrameMove(); +} diff --git a/xbmc/music/windows/GUIWindowVisualisation.h b/xbmc/music/windows/GUIWindowVisualisation.h new file mode 100644 index 0000000..ce9a198 --- /dev/null +++ b/xbmc/music/windows/GUIWindowVisualisation.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/GUIWindow.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/Stopwatch.h" + +class CGUIWindowVisualisation : + public CGUIWindow +{ +public: + CGUIWindowVisualisation(void); + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + void FrameMove() override; +protected: + EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override; + + CStopWatch m_initTimer; + CStopWatch m_lockedTimer; + bool m_bShowPreset; + MUSIC_INFO::CMusicInfoTag m_tag; // current tag info, for finding when the info manager updates +}; + diff --git a/xbmc/music/windows/MusicFileItemListModifier.cpp b/xbmc/music/windows/MusicFileItemListModifier.cpp new file mode 100644 index 0000000..c78de79 --- /dev/null +++ b/xbmc/music/windows/MusicFileItemListModifier.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicFileItemListModifier.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDbUrl.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +bool CMusicFileItemListModifier::CanModify(const CFileItemList &items) const +{ + if (items.IsMusicDb()) + return true; + + return false; +} + +bool CMusicFileItemListModifier::Modify(CFileItemList &items) const +{ + AddQueuingFolder(items); + return true; +} + +// Add an "* All ..." folder to the CFileItemList +// depending on the child node +void CMusicFileItemListModifier::AddQueuingFolder(CFileItemList& items) +{ + if (!items.IsMusicDb()) + return; + + auto directoryNode = CDirectoryNode::ParseURL(items.GetPath()); + + CFileItemPtr pItem; + + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(directoryNode->BuildPath())) + return; + + // always show "all" items by default + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWALLITEMS)) + return; + + // no need for "all" item when only one item + if (items.GetObjectCount() <= 1) + return; + + auto nodeChildType = directoryNode->GetChildType(); + + // No need for "*all" when overview node and child node is "albums" or "artists" + // without options (hence all albums or artists unfiltered). + if (directoryNode->GetType() == NODE_TYPE_OVERVIEW && + (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) && + musicUrl.GetOptions().empty()) + return; + // Smart playlist rules on parent node do not get applied to child nodes so no "*all" + // ! @Todo: Remove this allowing "*all" once rules do get applied to child nodes. + if (directoryNode->GetType() == NODE_TYPE_OVERVIEW && + (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) && + musicUrl.HasOption("xsp")) + return; + + switch (nodeChildType) + { + case NODE_TYPE_ARTIST: + pItem.reset(new CFileItem(g_localizeStrings.Get(15103))); // "All Artists" + musicUrl.AppendPath("-1/"); + pItem->SetPath(musicUrl.ToString()); + break; + + // All album related nodes + case NODE_TYPE_ALBUM: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_TOP100: + pItem.reset(new CFileItem(g_localizeStrings.Get(15102))); // "All Albums" + musicUrl.AppendPath("-1/"); + pItem->SetPath(musicUrl.ToString()); + break; + + // Disc node + case NODE_TYPE_DISC: + pItem.reset(new CFileItem(g_localizeStrings.Get(38075))); // "All Discs" + musicUrl.AppendPath("-1/"); + pItem->SetPath(musicUrl.ToString()); + break; + + default: + break; + } + + if (pItem) + { + pItem->m_bIsFolder = true; + pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom ? SortSpecialOnBottom : SortSpecialOnTop); + pItem->SetCanQueue(false); + pItem->SetLabelPreformatted(true); + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom) + items.Add(pItem); + else + items.AddFront(pItem, (items.Size() > 0 && items[0]->IsParentFolder()) ? 1 : 0); + } +} diff --git a/xbmc/music/windows/MusicFileItemListModifier.h b/xbmc/music/windows/MusicFileItemListModifier.h new file mode 100644 index 0000000..9a2190b --- /dev/null +++ b/xbmc/music/windows/MusicFileItemListModifier.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileItemListModifier.h" + +class CMusicFileItemListModifier : public IFileItemListModifier +{ +public: + CMusicFileItemListModifier() = default; + ~CMusicFileItemListModifier() override = default; + + bool CanModify(const CFileItemList &items) const override; + bool Modify(CFileItemList &items) const override; + +private: + static void AddQueuingFolder(CFileItemList & items); +}; |