diff options
Diffstat (limited to 'xbmc/music/infoscanner')
-rw-r--r-- | xbmc/music/infoscanner/CMakeLists.txt | 11 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicAlbumInfo.cpp | 52 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicAlbumInfo.h | 50 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicArtistInfo.cpp | 34 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicArtistInfo.h | 39 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicInfoScanner.cpp | 2338 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicInfoScanner.h | 265 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicInfoScraper.cpp | 202 | ||||
-rw-r--r-- | xbmc/music/infoscanner/MusicInfoScraper.h | 82 |
9 files changed, 3073 insertions, 0 deletions
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; +}; + +} |