1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "MusicInfoScanner.h"
10 
11 #include "FileItem.h"
12 #include "GUIInfoManager.h"
13 #include "GUIUserMessages.h"
14 #include "MusicAlbumInfo.h"
15 #include "MusicInfoScraper.h"
16 #include "NfoFile.h"
17 #include "ServiceBroker.h"
18 #include "TextureCache.h"
19 #include "Util.h"
20 #include "addons/AddonManager.h"
21 #include "addons/AddonSystemSettings.h"
22 #include "addons/Scraper.h"
23 #include "dialogs/GUIDialogExtendedProgressBar.h"
24 #include "dialogs/GUIDialogProgress.h"
25 #include "dialogs/GUIDialogSelect.h"
26 #include "dialogs/GUIDialogYesNo.h"
27 #include "events/EventLog.h"
28 #include "events/MediaLibraryEvent.h"
29 #include "filesystem/Directory.h"
30 #include "filesystem/File.h"
31 #include "filesystem/MusicDatabaseDirectory.h"
32 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
33 #include "filesystem/SmartPlaylistDirectory.h"
34 #include "guilib/GUIComponent.h"
35 #include "guilib/GUIKeyboardFactory.h"
36 #include "guilib/GUIWindowManager.h"
37 #include "guilib/LocalizeStrings.h"
38 #include "interfaces/AnnouncementManager.h"
39 #include "music/MusicLibraryQueue.h"
40 #include "music/MusicThumbLoader.h"
41 #include "music/MusicUtils.h"
42 #include "music/tags/MusicInfoTag.h"
43 #include "music/tags/MusicInfoTagLoaderFactory.h"
44 #include "settings/AdvancedSettings.h"
45 #include "settings/Settings.h"
46 #include "settings/SettingsComponent.h"
47 #include "threads/SystemClock.h"
48 #include "utils/Digest.h"
49 #include "utils/FileExtensionProvider.h"
50 #include "utils/StringUtils.h"
51 #include "utils/URIUtils.h"
52 #include "utils/Variant.h"
53 #include "utils/log.h"
54 
55 #include <algorithm>
56 #include <utility>
57 
58 using namespace MUSIC_INFO;
59 using namespace XFILE;
60 using namespace MUSICDATABASEDIRECTORY;
61 using namespace MUSIC_GRABBER;
62 using namespace ADDON;
63 using KODI::UTILITY::CDigest;
64 
CMusicInfoScanner()65 CMusicInfoScanner::CMusicInfoScanner()
66 : m_fileCountReader(this, "MusicFileCounter")
67 {
68   m_bStop = false;
69   m_currentItem=0;
70   m_itemCount=0;
71   m_flags = 0;
72 }
73 
74 CMusicInfoScanner::~CMusicInfoScanner() = default;
75 
Process()76 void CMusicInfoScanner::Process()
77 {
78   m_bStop = false;
79   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanStarted");
80   try
81   {
82     if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE))
83     {
84       CGUIDialogExtendedProgressBar* dialog =
85         CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
86       if (dialog)
87         m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
88     }
89 
90     // check if we only need to perform a cleaning
91     if (m_bClean && m_pathsToScan.empty())
92     {
93       CMusicLibraryQueue::GetInstance().CleanLibrary(false);
94       m_handle = NULL;
95       m_bRunning = false;
96 
97       return;
98     }
99 
100     unsigned int tick = XbmcThreads::SystemClockMillis();
101     m_musicDatabase.Open();
102     m_bCanInterrupt = true;
103 
104     if (m_scanType == 0) // load info from files
105     {
106       CLog::Log(LOGDEBUG, "%s - Starting scan", __FUNCTION__);
107 
108       if (m_handle)
109         m_handle->SetTitle(g_localizeStrings.Get(505));
110 
111       // Reset progress vars
112       m_currentItem=0;
113       m_itemCount=-1;
114 
115       // Create the thread to count all files to be scanned
116       if (m_handle)
117         m_fileCountReader.Create();
118 
119       // Database operations should not be canceled
120       // using Interrupt() while scanning as it could
121       // result in unexpected behaviour.
122       m_bCanInterrupt = false;
123       m_needsCleanup = false;
124 
125       bool commit = true;
126       for (const auto& it : m_pathsToScan)
127       {
128         if (!CDirectory::Exists(it) && !m_bClean)
129         {
130           /*
131            * Note that this will skip scanning (if m_bClean is disabled) if the directory really
132            * doesn't exist. Since the music scanner is fed with a list of existing paths from the DB
133            * and cleans out all songs under that path as its first step before re-adding files, if
134            * the entire source is offline we totally empty the music database in one go.
135            */
136           CLog::Log(LOGWARNING, "%s directory '%s' does not exist - skipping scan.", __FUNCTION__,
137                     it.c_str());
138           m_seenPaths.insert(it);
139           continue;
140         }
141 
142         // Clear list of albums added by this scan
143         m_albumsAdded.clear();
144         bool scancomplete = DoScan(it);
145         if (scancomplete)
146         {
147           if (m_albumsAdded.size() > 0)
148           {
149             // Set local art for added album disc sets and primary album artists
150             if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
151                     CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
152                 CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
153               RetrieveLocalArt();
154 
155             if (m_flags & SCAN_ONLINE)
156               // Download additional album and artist information for the recently added albums.
157               // This also identifies any local artist art if it exists, and gives it priority,
158               // otherwise it is set to the first available from the remote art that was scraped.
159               ScrapeInfoAddedAlbums();
160           }
161         }
162         else
163         {
164           commit = false;
165           break;
166         }
167       }
168 
169       if (commit)
170       {
171         CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
172 
173         if (m_needsCleanup)
174         {
175           if (m_handle)
176           {
177             m_handle->SetTitle(g_localizeStrings.Get(700));
178             m_handle->SetText("");
179           }
180 
181           m_musicDatabase.CleanupOrphanedItems();
182           m_musicDatabase.CheckArtistLinksChanged();
183 
184           if (m_handle)
185             m_handle->SetTitle(g_localizeStrings.Get(331));
186 
187           m_musicDatabase.Compress(false);
188         }
189       }
190 
191       m_fileCountReader.StopThread();
192 
193       m_musicDatabase.EmptyCache();
194 
195       tick = XbmcThreads::SystemClockMillis() - tick;
196       CLog::Log(LOGINFO, "My Music: Scanning for music info using worker thread, operation took %s",
197                 StringUtils::SecondsToTimeString(tick / 1000).c_str());
198     }
199     if (m_scanType == 1) // load album info
200     {
201       for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
202       {
203         CQueryParams params;
204         CDirectoryNode::GetDatabaseInfo(*it, params);
205         // Only scrape information for albums that have not been scraped before
206         // For refresh of information the lastscraped date is optionally clearered elsewhere
207         if (m_musicDatabase.HasAlbumBeenScraped(params.GetAlbumId()))
208           continue;
209 
210         CAlbum album;
211         m_musicDatabase.GetAlbum(params.GetAlbumId(), album);
212         if (m_handle)
213         {
214           float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
215           m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
216           m_handle->SetPercentage(percentage);
217         }
218 
219         // find album info
220         ADDON::ScraperPtr scraper;
221         if (!m_musicDatabase.GetScraper(album.idAlbum, CONTENT_ALBUMS, scraper))
222           continue;
223 
224         UpdateDatabaseAlbumInfo(album, scraper, false);
225 
226         if (m_bStop)
227           break;
228       }
229     }
230     if (m_scanType == 2) // load artist info
231     {
232       for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
233       {
234         CQueryParams params;
235         CDirectoryNode::GetDatabaseInfo(*it, params);
236         // Only scrape information for artists that have not been scraped before
237         // For refresh of information the lastscraped date is optionally clearered elsewhere
238         if (m_musicDatabase.HasArtistBeenScraped(params.GetArtistId()))
239             continue;
240 
241         CArtist artist;
242         m_musicDatabase.GetArtist(params.GetArtistId(), artist);
243         m_musicDatabase.GetArtistPath(artist, artist.strPath);
244 
245         if (m_handle)
246         {
247           float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
248           m_handle->SetText(artist.strArtist);
249           m_handle->SetPercentage(percentage);
250         }
251 
252         // find album info
253         ADDON::ScraperPtr scraper;
254         if (!m_musicDatabase.GetScraper(artist.idArtist, CONTENT_ARTISTS, scraper) || !scraper)
255           continue;
256 
257         UpdateDatabaseArtistInfo(artist, scraper, false);
258 
259         if (m_bStop)
260           break;
261       }
262     }
263     //propagate artist sort names to albums and songs
264     if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryArtistSortOnUpdate)
265       m_musicDatabase.UpdateArtistSortNames();
266   }
267   catch (...)
268   {
269     CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning.");
270   }
271   m_musicDatabase.Close();
272   CLog::Log(LOGDEBUG, "%s - Finished scan", __FUNCTION__);
273 
274   m_bRunning = false;
275   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanFinished");
276 
277   // we need to clear the musicdb cache and update any active lists
278   CUtil::DeleteMusicDatabaseDirectoryCache();
279   CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
280   CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
281 
282   if (m_handle)
283     m_handle->MarkFinished();
284   m_handle = NULL;
285 }
286 
Start(const std::string & strDirectory,int flags)287 void CMusicInfoScanner::Start(const std::string& strDirectory, int flags)
288 {
289   m_fileCountReader.StopThread();
290 
291   m_pathsToScan.clear();
292   m_seenPaths.clear();
293   m_albumsAdded.clear();
294   m_flags = flags;
295 
296   m_musicDatabase.Open();
297   // Check db sources match xml file and update if they don't
298   m_musicDatabase.UpdateSources();
299 
300   if (strDirectory.empty())
301   { // Scan all paths in the database.  We do this by scanning all paths in the
302     // db, and crossing them off the list as we go.
303     m_musicDatabase.GetPaths(m_pathsToScan);
304     m_idSourcePath = -1;
305   }
306   else
307   {
308     m_pathsToScan.insert(strDirectory);
309     m_idSourcePath = m_musicDatabase.GetSourceFromPath(strDirectory);
310   }
311   m_musicDatabase.Close();
312 
313   m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryCleanOnUpdate;
314 
315   m_scanType = 0;
316   m_bRunning = true;
317   Process();
318 }
319 
FetchAlbumInfo(const std::string & strDirectory,bool refresh)320 void CMusicInfoScanner::FetchAlbumInfo(const std::string& strDirectory,
321                                        bool refresh)
322 {
323   m_fileCountReader.StopThread();
324   m_pathsToScan.clear();
325 
326   CFileItemList items;
327   if (strDirectory.empty())
328   {
329     m_musicDatabase.Open();
330     m_musicDatabase.GetAlbumsNav("musicdb://albums/", items);
331     m_musicDatabase.Close();
332   }
333   else
334   {
335     CURL pathToUrl(strDirectory);
336 
337     if (pathToUrl.IsProtocol("musicdb"))
338     {
339       CQueryParams params;
340       CDirectoryNode::GetDatabaseInfo(strDirectory, params);
341       if (params.GetAlbumId() != -1)
342       {
343         //Add single album (id and path) as item to scan
344         CFileItemPtr item(new CFileItem(strDirectory, false));
345         item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum);
346         items.Add(item);
347       }
348       else
349       {
350         CMusicDatabaseDirectory dir;
351         NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
352         if (childtype == NODE_TYPE_ALBUM)
353           dir.GetDirectory(pathToUrl, items);
354       }
355     }
356     else if (StringUtils::EndsWith(strDirectory, ".xsp"))
357     {
358       CSmartPlaylistDirectory dir;
359       dir.GetDirectory(pathToUrl, items);
360     }
361   }
362 
363   m_musicDatabase.Open();
364   for (int i=0;i<items.Size();++i)
365   {
366     if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
367       continue;
368 
369     m_pathsToScan.insert(items[i]->GetPath());
370     if (refresh)
371     {
372       m_musicDatabase.ClearAlbumLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
373     }
374   }
375   m_musicDatabase.Close();
376 
377   m_scanType = 1;
378   m_bRunning = true;
379   Process();
380 }
381 
FetchArtistInfo(const std::string & strDirectory,bool refresh)382 void CMusicInfoScanner::FetchArtistInfo(const std::string& strDirectory,
383                                         bool refresh)
384 {
385   m_fileCountReader.StopThread();
386   m_pathsToScan.clear();
387   CFileItemList items;
388 
389   if (strDirectory.empty())
390   {
391     m_musicDatabase.Open();
392     m_musicDatabase.GetArtistsNav("musicdb://artists/", items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1);
393     m_musicDatabase.Close();
394   }
395   else
396   {
397     CURL pathToUrl(strDirectory);
398 
399     if (pathToUrl.IsProtocol("musicdb"))
400     {
401       CQueryParams params;
402       CDirectoryNode::GetDatabaseInfo(strDirectory, params);
403       if (params.GetArtistId() != -1)
404       {
405         //Add single artist (id and path) as item to scan
406         CFileItemPtr item(new CFileItem(strDirectory, false));
407         item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeArtist);
408         items.Add(item);
409       }
410       else
411       {
412         CMusicDatabaseDirectory dir;
413         NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
414         if (childtype == NODE_TYPE_ARTIST)
415           dir.GetDirectory(pathToUrl, items);
416       }
417     }
418     else if (StringUtils::EndsWith(strDirectory, ".xsp"))
419     {
420       CSmartPlaylistDirectory dir;
421       dir.GetDirectory(pathToUrl, items);
422     }
423   }
424 
425   m_musicDatabase.Open();
426   for (int i=0;i<items.Size();++i)
427   {
428     if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
429       continue;
430 
431     m_pathsToScan.insert(items[i]->GetPath());
432     if (refresh)
433     {
434       m_musicDatabase.ClearArtistLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
435     }
436   }
437   m_musicDatabase.Close();
438 
439   m_scanType = 2;
440   m_bRunning = true;
441   Process();
442 }
443 
Stop()444 void CMusicInfoScanner::Stop()
445 {
446   if (m_bCanInterrupt)
447     m_musicDatabase.Interrupt();
448 
449   m_bStop = true;
450 }
451 
OnDirectoryScanned(const std::string & strDirectory)452 static void OnDirectoryScanned(const std::string& strDirectory)
453 {
454   CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
455   msg.SetStringParam(strDirectory);
456   CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
457 }
458 
Prettify(const std::string & strDirectory)459 static std::string Prettify(const std::string& strDirectory)
460 {
461   CURL url(strDirectory);
462 
463   return CURL::Decode(url.GetWithoutUserDetails());
464 }
465 
DoScan(const std::string & strDirectory)466 bool CMusicInfoScanner::DoScan(const std::string& strDirectory)
467 {
468   if (m_handle)
469   {
470     m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
471     m_handle->SetText(Prettify(strDirectory));
472   }
473 
474   std::set<std::string>::const_iterator it = m_seenPaths.find(strDirectory);
475   if (it != m_seenPaths.end())
476     return true;
477 
478   m_seenPaths.insert(strDirectory);
479 
480   // Discard all excluded files defined by m_musicExcludeRegExps
481   const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
482 
483   if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
484     return true;
485 
486   if (HasNoMedia(strDirectory))
487     return true;
488 
489   // load subfolder
490   CFileItemList items;
491   CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|.jpg|.tbn|.lrc|.cdg", DIR_FLAG_DEFAULTS);
492 
493   // sort and get the path hash.  Note that we don't filter .cue sheet items here as we want
494   // to detect changes in the .cue sheet as well.  The .cue sheet items only need filtering
495   // if we have a changed hash.
496   items.Sort(SortByLabel, SortOrderAscending);
497   std::string hash;
498   GetPathHash(items, hash);
499 
500   // check whether we need to rescan or not
501   std::string dbHash;
502   if ((m_flags & SCAN_RESCAN) || !m_musicDatabase.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
503   { // path has changed - rescan
504     if (dbHash.empty())
505       CLog::Log(LOGDEBUG, "%s Scanning dir '%s' as not in the database", __FUNCTION__, CURL::GetRedacted(strDirectory).c_str());
506     else
507       CLog::Log(LOGDEBUG, "%s Rescanning dir '%s' due to change", __FUNCTION__, CURL::GetRedacted(strDirectory).c_str());
508 
509     if (m_handle)
510       m_handle->SetTitle(g_localizeStrings.Get(505)); //"Loading media information from files..."
511 
512     // filter items in the sub dir (for .cue sheet support)
513     items.FilterCueItems();
514     items.Sort(SortByLabel, SortOrderAscending);
515 
516     // and then scan in the new information from tags
517     if (RetrieveMusicInfo(strDirectory, items) > 0)
518     {
519       if (m_handle)
520         OnDirectoryScanned(strDirectory);
521     }
522 
523     // save information about this folder
524     m_musicDatabase.SetPathHash(strDirectory, hash);
525   }
526   else
527   { // path is the same - no need to rescan
528     CLog::Log(LOGDEBUG, "%s Skipping dir '%s' due to no change", __FUNCTION__, CURL::GetRedacted(strDirectory).c_str());
529     m_currentItem += CountFiles(items, false);  // false for non-recursive
530 
531     // updated the dialog with our progress
532     if (m_handle)
533     {
534       if (m_itemCount>0)
535         m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
536       OnDirectoryScanned(strDirectory);
537     }
538   }
539 
540   // now scan the subfolders
541   for (int i = 0; i < items.Size(); ++i)
542   {
543     CFileItemPtr pItem = items[i];
544 
545     if (m_bStop)
546       break;
547     // if we have a directory item (non-playlist) we then recurse into that folder
548     if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList())
549     {
550       std::string strPath=pItem->GetPath();
551       if (!DoScan(strPath))
552       {
553         m_bStop = true;
554       }
555     }
556   }
557   return !m_bStop;
558 }
559 
ScanTags(const CFileItemList & items,CFileItemList & scannedItems)560 CInfoScanner::INFO_RET CMusicInfoScanner::ScanTags(const CFileItemList& items,
561                                                    CFileItemList& scannedItems)
562 {
563   std::vector<std::string> regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
564 
565   for (int i = 0; i < items.Size(); ++i)
566   {
567     if (m_bStop)
568       return INFO_CANCELLED;
569 
570     CFileItemPtr pItem = items[i];
571 
572     if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps))
573       continue;
574 
575     if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics())
576       continue;
577 
578     m_currentItem++;
579 
580     CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
581     if (!tag.Loaded())
582     {
583       std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem));
584       if (nullptr != pLoader)
585         pLoader->Load(pItem->GetPath(), tag);
586     }
587 
588     if (m_handle && m_itemCount>0)
589       m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
590 
591     if (!tag.Loaded() && !pItem->HasCueDocument())
592     {
593       CLog::Log(LOGDEBUG, "%s - No tag found for: %s", __FUNCTION__, pItem->GetPath().c_str());
594       continue;
595     }
596     else
597     {
598       if (!tag.GetCueSheet().empty())
599         pItem->LoadEmbeddedCue();
600     }
601 
602     if (pItem->HasCueDocument())
603       pItem->LoadTracksFromCueDocument(scannedItems);
604     else
605       scannedItems.Add(pItem);
606   }
607   return INFO_ADDED;
608 }
609 
SortSongsByTrack(const CSong & song,const CSong & song2)610 static bool SortSongsByTrack(const CSong& song, const CSong& song2)
611 {
612   return song.iTrack < song2.iTrack;
613 }
614 
FileItemsToAlbums(CFileItemList & items,VECALBUMS & albums,MAPSONGS * songsMap)615 void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */)
616 {
617   /*
618    * Step 1: Convert the FileItems into Songs.
619    * If they're MB tagged, create albums directly from the FileItems.
620    * If they're non-MB tagged, index them by album name ready for step 2.
621    */
622   std::map<std::string, VECSONGS> songsByAlbumNames;
623   for (int i = 0; i < items.Size(); ++i)
624   {
625     CMusicInfoTag& tag = *items[i]->GetMusicInfoTag();
626     CSong song(*items[i]);
627 
628     // keep the db-only fields intact on rescan...
629     if (songsMap != NULL)
630     {
631       // Match up item to songs in library previously scanned with this path
632       MAPSONGS::iterator songlist = songsMap->find(items[i]->GetPath());
633       if (songlist != songsMap->end())
634       {
635         VECSONGS::iterator foundsong;
636         if (songlist->second.size() == 1)
637           foundsong = songlist->second.begin();
638         else
639         {
640           // When filename mapped to multiple songs it is from cuesheet, match on disc/track number
641           int disctrack = tag.GetTrackAndDiscNumber();
642           foundsong = std::find_if(songlist->second.begin(), songlist->second.end(),
643                                    [&](const CSong& song) { return disctrack == song.iTrack; });
644         }
645         if (foundsong != songlist->second.end())
646         {
647           song.idSong = foundsong->idSong; // Reuse ID
648           song.dateNew = foundsong->dateNew; // Keep date originally created
649           song.iTimesPlayed = foundsong->iTimesPlayed;
650           song.lastPlayed = foundsong->lastPlayed;
651           if (song.rating == 0)
652             song.rating = foundsong->rating;
653           if (song.userrating == 0)
654             song.userrating = foundsong->userrating;
655           if (song.strThumb.empty())
656             song.strThumb = foundsong->strThumb;
657         }
658       }
659     }
660 
661     if (!tag.GetMusicBrainzAlbumID().empty())
662     {
663       VECALBUMS::iterator it;
664       for (it = albums.begin(); it != albums.end(); ++it)
665         if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID())
666           break;
667 
668       if (it == albums.end())
669       {
670         CAlbum album(*items[i]);
671         album.songs.push_back(song);
672         albums.push_back(album);
673       }
674       else
675         it->songs.push_back(song);
676     }
677     else
678       songsByAlbumNames[tag.GetAlbum()].push_back(song);
679   }
680 
681   /*
682    Step 2: Split into unique albums based on album name and album artist
683    In the case where the album artist is unknown, we use the primary artist
684    (i.e. first artist from each song).
685    */
686   for (auto& songsByAlbumName : songsByAlbumNames)
687   {
688     VECSONGS& songs = songsByAlbumName.second;
689     // sort the songs by tracknumber to identify duplicate track numbers
690     sort(songs.begin(), songs.end(), SortSongsByTrack);
691 
692     // map the songs to their primary artists
693     bool tracksOverlap = false;
694     bool hasAlbumArtist = false;
695     bool isCompilation = true;
696     std::string old_DiscSubtitle;
697 
698     std::map<std::string, std::vector<CSong *> > artists;
699     for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song)
700     {
701       // test for song overlap
702       if (song != songs.begin() && song->iTrack == (song - 1)->iTrack)
703         tracksOverlap = true;
704 
705       if (!song->bCompilation)
706         isCompilation = false;
707 
708       if (song->strDiscSubtitle != old_DiscSubtitle)
709         old_DiscSubtitle = song->strDiscSubtitle;
710 
711       // get primary artist
712       std::string primary;
713       if (!song->GetAlbumArtist().empty())
714       {
715         primary = song->GetAlbumArtist()[0];
716         hasAlbumArtist = true;
717       }
718       else if (!song->artistCredits.empty())
719         primary = song->artistCredits.begin()->GetArtist();
720 
721       // add to the artist map
722       artists[primary].push_back(&(*song));
723     }
724 
725     /*
726     We have a Various Artists compilation if
727     1. album name is non-empty AND
728     2a. no tracks overlap OR
729     2b. all tracks are marked as part of compilation AND
730     3a. a unique primary artist is specified as "various", "various artists" or the localized value
731     OR
732     3b. we have at least two primary artists and no album artist specified.
733     */
734     std::string various = g_localizeStrings.Get(340); // Various Artists
735     bool compilation =
736         !songsByAlbumName.first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a
737     if (artists.size() == 1)
738     {
739       std::string artist = artists.begin()->first; StringUtils::ToLower(artist);
740       if (!StringUtils::EqualsNoCase(artist, "various") &&
741         !StringUtils::EqualsNoCase(artist, "various artists") &&
742         !StringUtils::EqualsNoCase(artist, various)) // 3a
743         compilation = false;
744       else
745         // Grab name for use in "various artist" artist
746         various = artists.begin()->first;
747     }
748     else if (hasAlbumArtist) // 3b
749       compilation = false;
750 
751     // Such a compilation album is stored under a unique artist that matches on Musicbrainz ID
752     // the "various artists" artist for music tagged with mbids.
753     if (compilation)
754     {
755       CLog::Log(LOGDEBUG, "Album '%s' is a compilation as there's no overlapping tracks and %s",
756                 songsByAlbumName.first.c_str(),
757                 hasAlbumArtist ? "the album artist is 'Various'"
758                                : "there is more than one unique artist");
759       // Clear song artists from artists map, put songs under "various artists" mbid entry
760       artists.clear();
761       for (auto& song : songs)
762         artists[VARIOUSARTISTS_MBID].push_back(&song);
763     }
764 
765     /*
766     We also have a compilation album when album name is non-empty and ALL tracks are marked as part of
767     a compilation even if an album artist is given, or all songs have the same primary artist. For
768     example an anthology - a collection of recordings from various old sources
769     combined together such as a "best of", retrospective or rarities type release.
770 
771     Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b.
772     The album artist can be determined just like any normal album.
773     */
774     if (!compilation && !songsByAlbumName.first.empty() && isCompilation)
775     {
776       compilation = true;
777       CLog::Log(LOGDEBUG,
778                 "Album '%s' is a compilation as all songs are marked as part of a compilation",
779                 songsByAlbumName.first.c_str());
780     }
781 
782     /*
783      Step 3: Find the common albumartist for each song and assign
784      albumartist to those tracks that don't have it set.
785      */
786     for (auto& j : artists)
787     {
788       /* Find the common artist(s) for these songs (grouped under primary artist).
789       Various artist compilations already under the unique "various artists" mbid.
790       Take from albumartist tag when present, or use artist tag.
791       When from albumartist tag also check albumartistsort tag and take first non-empty value
792       */
793       std::vector<CSong*>& artistSongs = j.second;
794       std::vector<std::string> common;
795       std::string albumartistsort;
796       if (artistSongs.front()->GetAlbumArtist().empty())
797         common = artistSongs.front()->GetArtist();
798       else
799       {
800         common = artistSongs.front()->GetAlbumArtist();
801         albumartistsort = artistSongs.front()->GetAlbumArtistSort();
802       }
803       for (std::vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k)
804       {
805         unsigned int match = 0;
806         std::vector<std::string> compare;
807         if ((*k)->GetAlbumArtist().empty())
808           compare = (*k)->GetArtist();
809         else
810         {
811           compare = (*k)->GetAlbumArtist();
812           if (albumartistsort.empty())
813             albumartistsort = (*k)->GetAlbumArtistSort();
814         }
815         for (; match < common.size() && match < compare.size(); match++)
816         {
817           if (compare[match] != common[match])
818             break;
819         }
820         common.erase(common.begin() + match, common.end());
821       }
822       if (j.first == VARIOUSARTISTS_MBID)
823       {
824         common.clear();
825         common.emplace_back(VARIOUSARTISTS_MBID);
826       }
827 
828       /*
829        Step 4: Assign the album artist for each song that doesn't have it set
830        and add to the album vector
831        */
832       CAlbum album;
833       album.strAlbum = songsByAlbumName.first;
834 
835       //Split the albumartist sort string to try and get sort names for individual artists
836       std::vector<std::string> sortnames = StringUtils::Split(albumartistsort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
837       if (sortnames.size() != common.size())
838           // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
839         sortnames = StringUtils::SplitMulti(sortnames, { ";", ":", "|", "#" });
840 
841       for (size_t i = 0; i < common.size(); i++)
842       {
843         if (common[i] == VARIOUSARTISTS_MBID)
844           /* Treat "various", "various artists" and the localized equivalent name as the same
845           album artist as the artist with Musicbrainz ID 89ad4ac3-39f7-470e-963a-56509c546377.
846           If adding this artist for the first time then the name will be set to either the primary
847           artist read from tags when 3a, or the localized value for "various artists" when not 3a.
848           This means that tag values are no longer translated into the current langauge.
849           */
850           album.artistCredits.emplace_back(various, VARIOUSARTISTS_MBID);
851         else
852         {
853           album.artistCredits.emplace_back(StringUtils::Trim(common[i]));
854           // Set artist sort name providing we have as many as we have artists,
855           // otherwise something is wrong with them so ignore rather than guess.
856           if (sortnames.size() == common.size())
857             album.artistCredits.back().SetSortName(StringUtils::Trim(sortnames[i]));
858         }
859       }
860       album.bCompilation = compilation;
861       for (auto& k : artistSongs)
862       {
863         if (k->GetAlbumArtist().empty())
864           k->SetAlbumArtist(common);
865         //! @todo in future we may wish to union up the genres, for now we assume they're the same
866         album.genre = k->genre;
867         album.strArtistSort = k->GetAlbumArtistSort();
868         // in addition, we may want to use release date as discriminating between albums
869         album.strReleaseDate = k->strReleaseDate,
870         album.strLabel = k->strRecordLabel;
871         album.strType = k->strAlbumType;
872         album.songs.push_back(*k);
873       }
874       albums.push_back(album);
875     }
876   }
877 }
878 
879 CInfoScanner::INFO_RET
UpdateAlbumInfo(CAlbum & album,const ADDON::ScraperPtr & scraper,bool bAllowSelection,CGUIDialogProgress * pDialog)880 CMusicInfoScanner::UpdateAlbumInfo(CAlbum& album,
881                                    const ADDON::ScraperPtr& scraper,
882                                    bool bAllowSelection,
883                                    CGUIDialogProgress* pDialog)
884 {
885   m_musicDatabase.Open();
886   INFO_RET result = UpdateDatabaseAlbumInfo(album, scraper, bAllowSelection, pDialog);
887   m_musicDatabase.Close();
888   return result;
889 }
890 
891 CInfoScanner::INFO_RET
UpdateArtistInfo(CArtist & artist,const ADDON::ScraperPtr & scraper,bool bAllowSelection,CGUIDialogProgress * pDialog)892 CMusicInfoScanner::UpdateArtistInfo(CArtist& artist,
893                                     const ADDON::ScraperPtr& scraper,
894                                     bool bAllowSelection,
895                                     CGUIDialogProgress* pDialog)
896 {
897   m_musicDatabase.Open();
898   INFO_RET result = UpdateDatabaseArtistInfo(artist, scraper, bAllowSelection, pDialog);
899   m_musicDatabase.Close();
900   return result;
901 }
902 
RetrieveMusicInfo(const std::string & strDirectory,CFileItemList & items)903 int CMusicInfoScanner::RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items)
904 {
905   MAPSONGS songsMap;
906 
907   // get all information for all files in current directory from database, and remove them
908   if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap))
909     m_needsCleanup = true;
910 
911   CFileItemList scannedItems;
912   if (ScanTags(items, scannedItems) == INFO_CANCELLED || scannedItems.Size() == 0)
913     return 0;
914 
915   VECALBUMS albums;
916   FileItemsToAlbums(scannedItems, albums, &songsMap);
917 
918   /*
919   Set thumb for songs and, if only one album in folder, store the thumb for
920   the album (music db) and the folder path (in Textures db) too.
921   The album and path thumb is either set to the folder art, or failing that to
922   the art embedded in the first music file.
923   Song thumb is only set when it varies, otherwise it is cleared so that it will
924   fallback to the album art (that may be from the first file, or that of the
925   folder or set later by scraping from NFO files or remote sources). Clearing
926   saves caching repeats of the same image.
927 
928   However even if all songs are from one album this may not be the album
929   folder. It could be just a subfolder containing some of the songs from a disc
930   set e.g. CD1, CD2 etc., or the album could spread across many folders.  In
931   this case the album art gets reset every time a folder with songs from just
932   that album is processed, and needs to be corrected later once all the parts
933   of the album have been scanned.
934   */
935   FindArtForAlbums(albums, items.GetPath());
936 
937   /* Strategy: Having scanned tags and made a list of albums, add them to the library. Only then try
938   to scrape additional album and artist information. Music is often tagged to a mixed standard
939   - some albums have mbid tags, some don't. Once all the music files have been added to the library,
940   the mbid for an artist will be known even if it was only tagged on one song. The artist is best
941   scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
942   That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
943   even when it was not in the tagging for that album.
944 
945   Doing scraping, generally the slower activity, in the background after scanning has fully populated
946   the library also means that the user can use their library to select music to play sooner.
947   */
948 
949   int numAdded = 0;
950 
951   // Add all albums to the library, and hence any new song or album artists or other contributors
952   for (auto& album : albums)
953   {
954     if (m_bStop)
955       break;
956 
957     // mark albums without a title as singles
958     if (album.strAlbum.empty())
959       album.releaseType = CAlbum::Single;
960 
961     album.strPath = strDirectory;
962     m_musicDatabase.AddAlbum(album, m_idSourcePath);
963     m_albumsAdded.insert(album.idAlbum);
964 
965     numAdded += static_cast<int>(album.songs.size());
966   }
967   return numAdded;
968 }
969 
ScrapeInfoAddedAlbums()970 void MUSIC_INFO::CMusicInfoScanner::ScrapeInfoAddedAlbums()
971 {
972   /* Strategy: Having scanned tags, make a list of albums and add them to the library, only then try
973   to scrape additional album and artist information. Music is often tagged to a mixed standard
974   - some albums have mbid tags, some don't. Once all the music files have been added to the library,
975   the mbid for an artist will be known even if it was only tagged on one song. The artist is best
976   scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
977   That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
978   even when it was not in the tagging for that album.
979 
980   Doing scraping, generally the slower activity, in the background after scanning has fully populated
981   the library also means that the user can use their library to select music to play sooner.
982   */
983 
984   /* Scrape additional album and artist data.
985   For albums and artists without mbids, matching on album-artist pair can
986   be used to identify artist with greater accuracy than artist name alone.
987   Artist mbid returned by album scraper is used if we do not already have it.
988   Hence scrape album then related artists.
989   */
990   ADDON::AddonPtr addon;
991 
992   ADDON::ScraperPtr albumScraper;
993   ADDON::ScraperPtr artistScraper;
994   if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::ADDON_SCRAPER_ALBUMS, addon))
995     albumScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
996 
997   if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::ADDON_SCRAPER_ARTISTS, addon))
998     artistScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
999 
1000   bool albumartistsonly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS);
1001 
1002   if (!albumScraper || !artistScraper)
1003     return;
1004 
1005   int i = 0;
1006   std::set<int> artists;
1007   for (auto albumId : m_albumsAdded)
1008   {
1009     i++;
1010     if (m_bStop)
1011       break;
1012     // Scrape album data
1013     CAlbum album;
1014     if (!m_musicDatabase.HasAlbumBeenScraped(albumId))
1015     {
1016       if (m_handle)
1017       {
1018         m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
1019         m_handle->SetProgress(i, static_cast<int>(m_albumsAdded.size()));
1020       }
1021 
1022       // Fetch any artist mbids for album artist(s) and song artists when scraping those too.
1023       m_musicDatabase.GetAlbum(albumId, album, !albumartistsonly);
1024       UpdateDatabaseAlbumInfo(album, albumScraper, false);
1025 
1026       // Scrape information for artists that have not been scraped before, avoiding repeating
1027       // unsuccessful attempts for every album and song.
1028       for (const auto &artistCredit : album.artistCredits)
1029       {
1030         if (m_bStop)
1031           break;
1032 
1033         if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
1034           artists.find(artistCredit.GetArtistId()) == artists.end())
1035         {
1036           artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
1037           CArtist artist;
1038           m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
1039           UpdateDatabaseArtistInfo(artist, artistScraper, false);
1040         }
1041       }
1042       // Only scrape song artists if they are being displayed in artists node by default
1043       if (!albumartistsonly)
1044       {
1045         for (auto &song : album.songs)
1046         {
1047           if (m_bStop)
1048             break;
1049           for (const auto &artistCredit : song.artistCredits)
1050           {
1051             if (m_bStop)
1052               break;
1053 
1054             CMusicArtistInfo musicArtistInfo;
1055             if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
1056               artists.find(artistCredit.GetArtistId()) == artists.end())
1057             {
1058               artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
1059               CArtist artist;
1060               m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
1061               UpdateDatabaseArtistInfo(artist, artistScraper, false);
1062             }
1063           }
1064         }
1065       }
1066     }
1067   }
1068 }
1069 
1070 /*
1071   Set thumb for songs and the album(if only one album in folder).
1072   The album thumb is either set to the folder art, or failing that to the art
1073   embedded in the first music file. However this does not allow for there being
1074   other folders with more songs from the album e.g. this was a subfolder CD1
1075   and there is CD2 etc. yet to be processed
1076   Song thumb is only set when it varies, otherwise it is cleared so that it will
1077   fallback to the album art(that may be from the first file, or that of the
1078   folder or set later by scraping from NFO files or remote sources).Clearing
1079   saves caching repeats of the same image.
1080 */
FindArtForAlbums(VECALBUMS & albums,const std::string & path)1081 void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const std::string &path)
1082 {
1083   /*
1084    If there's a single album in the folder, then art can be taken from
1085    the folder art.
1086    */
1087   std::string albumArt;
1088   if (albums.size() == 1)
1089   {
1090     CFileItem album(path, true);
1091     /*
1092      If we are scanning a directory served over http(s) the root directory for an album will set
1093      IsInternetStream to true which prevents scanning it for art.  As we can't reach this point
1094      without having read some tags (and tags are not read from streams) we can safely check for
1095      that case and set the IsHTTPDirectory property to enable scanning for art.
1096     */
1097     if (StringUtils::StartsWithNoCase(path, "http") && StringUtils::EndsWith(path, "/"))
1098       album.SetProperty("IsHTTPDirectory", true);
1099     albumArt = album.GetUserMusicThumb(true);
1100     if (!albumArt.empty())
1101       albums[0].art["thumb"] = albumArt;
1102   }
1103   for (auto& album : albums)
1104   {
1105     if (albums.size() != 1)
1106       albumArt = "";
1107 
1108     /*
1109      Find art that is common across these items
1110      If we find a single art image we treat it as the album art
1111      and discard song art else we use first as album art and
1112      keep everything as song art.
1113      */
1114     bool singleArt = true;
1115     CSong *art = NULL;
1116     for (auto& song : album.songs)
1117     {
1118       if (song.HasArt())
1119       {
1120         if (art && !art->ArtMatches(song))
1121         {
1122           singleArt = false;
1123           break;
1124         }
1125         if (!art)
1126           art = &song;
1127       }
1128     }
1129 
1130     /*
1131       assign the first art found to the album - better than no art at all
1132     */
1133 
1134     if (art && albumArt.empty())
1135     {
1136       if (!art->strThumb.empty())
1137         albumArt = art->strThumb;
1138       else
1139         albumArt = CTextureUtils::GetWrappedImageURL(art->strFileName, "music");
1140     }
1141 
1142     if (!albumArt.empty())
1143       album.art["thumb"] = albumArt;
1144 
1145     if (singleArt)
1146     { //if singleArt then we can clear the artwork for all songs
1147       for (auto& k : album.songs)
1148         k.strThumb.clear();
1149     }
1150     else
1151     { // more than one piece of art was found for these songs, so cache per song
1152       for (auto& k : album.songs)
1153       {
1154         if (k.strThumb.empty() && !k.embeddedArt.Empty())
1155           k.strThumb = CTextureUtils::GetWrappedImageURL(k.strFileName, "music");
1156       }
1157     }
1158   }
1159   if (albums.size() == 1 && !albumArt.empty())
1160   {
1161     // assign to folder thumb as well
1162     CFileItem albumItem(path, true);
1163     CMusicThumbLoader loader;
1164     loader.SetCachedImage(albumItem, "thumb", albumArt);
1165   }
1166 }
1167 
RetrieveLocalArt()1168 void MUSIC_INFO::CMusicInfoScanner::RetrieveLocalArt()
1169 {
1170   if (m_handle)
1171   {
1172     m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
1173    //!@todo: title = Checking for local art
1174   }
1175 
1176   std::set<int> artistsArtDone; // artists processed to avoid unsuccessful repeats
1177   int count = 0;
1178   for (auto albumId : m_albumsAdded)
1179   {
1180     count++;
1181     if (m_bStop)
1182       break;
1183     CAlbum album;
1184     m_musicDatabase.GetAlbum(albumId, album, false);
1185     if (m_handle)
1186     {
1187       m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
1188       m_handle->SetProgress(count, static_cast<int>(m_albumsAdded.size()));
1189     }
1190 
1191     /*
1192     Automatically fetch local art from album folder and any disc sets subfolders
1193 
1194     Providing all songs from an album are are under a unique common album
1195     folder (no songs from other albums) then thumb has been set to local art,
1196     or failing that to embedded art, during scanning by FindArtForAlbums().
1197     But when songs are also spread over multiple subfolders within it e.g. disc
1198     sets, it will have been set to either the art of the last subfolder that was
1199     processed (if there is any), or from the first song in that subfolder with
1200     embedded art (if there is any). To correct this and find any thumb in the
1201     (common) album folder add "thumb" to those missing.
1202     */
1203     AddAlbumArtwork(album);
1204 
1205     /*
1206     Local album artist art
1207 
1208     Look in the nominated "Artist Information Folder" for thumbs and fanart.
1209     Failing that, for backward compatibility, fallback to the folder immediately
1210     above the album folder.
1211     It can only fallback if the album has a unique folder, and can only do so
1212     for the first album artist if the album is a collaboration e.g. composer,
1213     conductor, orchestra, or by several pop artists in their own right.
1214     Avoids repeatedly processing the same artist by maintaining a set.
1215 
1216     Adding the album may have added new artists, or provide art for an existing
1217     (song) artist, but does not replace any artwork already set. Hence once art
1218     has been found for an album artist, art is not searched for in other folders.
1219 
1220     It will find art for "various artists", if artwork is located above the
1221     folder containing compilatons.
1222     */
1223     for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
1224     {
1225       if (m_bStop)
1226         break;
1227       int idArtist = artistCredit->GetArtistId();
1228       if (artistsArtDone.find(idArtist) == artistsArtDone.end())
1229       {
1230         artistsArtDone.insert(idArtist); // Artist processed
1231 
1232         // Get artist and subfolder within the Artist Information Folder
1233         CArtist artist;
1234         m_musicDatabase.GetArtist(idArtist, artist);
1235         m_musicDatabase.GetArtistPath(artist, artist.strPath);
1236         // Location of local art
1237         std::string artfolder;
1238         if (CDirectory::Exists(artist.strPath))
1239           // When subfolder exists that is only place we look for local art
1240           artfolder = artist.strPath;
1241         else if (!album.strPath.empty() && artistCredit == album.artistCredits.begin())
1242         {
1243           // If no individual artist subfolder has been found, for primary
1244           // album artist only look in the folder immediately above the album
1245           // folder. Not using GetOldArtistPath here because may not have not
1246           // have scanned all the albums yet.
1247           artfolder = URIUtils::GetParentPath(album.strPath);
1248         }
1249         AddArtistArtwork(artist, artfolder);
1250       }
1251     }
1252   }
1253 }
1254 
GetPathHash(const CFileItemList & items,std::string & hash)1255 int CMusicInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
1256 {
1257   // Create a hash based on the filenames, filesize and filedate.  Also count the number of files
1258   if (0 == items.Size()) return 0;
1259   CDigest digest{CDigest::Type::MD5};
1260   int count = 0;
1261   for (int i = 0; i < items.Size(); ++i)
1262   {
1263     const CFileItemPtr pItem = items[i];
1264     digest.Update(pItem->GetPath());
1265     digest.Update((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1266     KODI::TIME::FileTime time = pItem->m_dateTime;
1267     digest.Update((unsigned char*)&time, sizeof(KODI::TIME::FileTime));
1268     if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
1269       count++;
1270   }
1271   hash = digest.Finalize();
1272   return count;
1273 }
1274 
1275 CInfoScanner::INFO_RET
UpdateDatabaseAlbumInfo(CAlbum & album,const ADDON::ScraperPtr & scraper,bool bAllowSelection,CGUIDialogProgress * pDialog)1276 CMusicInfoScanner::UpdateDatabaseAlbumInfo(CAlbum& album,
1277                                            const ADDON::ScraperPtr& scraper,
1278                                            bool bAllowSelection,
1279                                            CGUIDialogProgress* pDialog /* = NULL */)
1280 {
1281   if (!scraper)
1282     return INFO_ERROR;
1283 
1284   CMusicAlbumInfo albumInfo;
1285   INFO_RET albumDownloadStatus(INFO_CANCELLED);
1286   std::string origArtist(album.GetAlbumArtistString());
1287   std::string origAlbum(album.strAlbum);
1288 
1289   bool stop(false);
1290   while (!stop)
1291   {
1292     stop = true;
1293     CLog::Log(LOGDEBUG, "%s downloading info for: %s", __FUNCTION__, album.strAlbum.c_str());
1294     albumDownloadStatus = DownloadAlbumInfo(album, scraper, albumInfo, !bAllowSelection, pDialog);
1295     if (albumDownloadStatus == INFO_NOT_FOUND)
1296     {
1297       if (pDialog && bAllowSelection)
1298       {
1299         std::string strTempAlbum(album.strAlbum);
1300         if (!CGUIKeyboardFactory::ShowAndGetInput(strTempAlbum, CVariant{ g_localizeStrings.Get(16011) }, false))
1301           albumDownloadStatus = INFO_CANCELLED;
1302         else
1303         {
1304           std::string strTempArtist(album.GetAlbumArtistString());
1305           if (!CGUIKeyboardFactory::ShowAndGetInput(strTempArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
1306             albumDownloadStatus = INFO_CANCELLED;
1307           else
1308           {
1309             album.strAlbum = strTempAlbum;
1310             album.strArtistDesc = strTempArtist;
1311             stop = false;
1312           }
1313         }
1314       }
1315       else
1316       {
1317         CServiceBroker::GetEventLog().Add(EventPtr(
1318             new CMediaLibraryEvent(MediaTypeAlbum, album.strPath, 24146,
1319                                    StringUtils::Format(g_localizeStrings.Get(24147).c_str(),
1320                                                        MediaTypeAlbum, album.strAlbum.c_str()),
1321                                    CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType()),
1322                                    CURL::GetRedacted(album.strPath), EventLevel::Warning)));
1323       }
1324     }
1325   }
1326 
1327   // Restore original album and artist name, possibly changed by manual entry
1328   // to get info but should not change outside merge
1329   album.strAlbum = origAlbum;
1330   album.strArtistDesc = origArtist;
1331 
1332   if (albumDownloadStatus == INFO_ADDED)
1333   {
1334     bool overridetags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS);
1335     // Remove art accidentally set by the Python scraper, it only provides URLs of possible artwork
1336     // Art is selected later applying whitelist and other art preferences
1337     albumInfo.GetAlbum().art.clear();
1338     album.MergeScrapedAlbum(albumInfo.GetAlbum(), overridetags);
1339     m_musicDatabase.UpdateAlbum(album);
1340     albumInfo.SetLoaded(true);
1341   }
1342 
1343   // Check album art.
1344   // Fill any gaps with local art files or use first available from scraped URL list (when it has
1345   // been successfuly scraped) as controlled by whitelist. Do this even when no info added
1346   // (cancelled, not found or error), there may be new local art files.
1347   if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1348           CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
1349           CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE &&
1350       AddAlbumArtwork(album))
1351     albumDownloadStatus = INFO_ADDED; // Local art added
1352 
1353   return albumDownloadStatus;
1354 }
1355 
1356 CInfoScanner::INFO_RET
UpdateDatabaseArtistInfo(CArtist & artist,const ADDON::ScraperPtr & scraper,bool bAllowSelection,CGUIDialogProgress * pDialog)1357 CMusicInfoScanner::UpdateDatabaseArtistInfo(CArtist& artist,
1358                                             const ADDON::ScraperPtr& scraper,
1359                                             bool bAllowSelection,
1360                                             CGUIDialogProgress* pDialog /* = NULL */)
1361 {
1362   if (!scraper)
1363     return INFO_ERROR;
1364 
1365   CMusicArtistInfo artistInfo;
1366   INFO_RET artistDownloadStatus(INFO_CANCELLED);
1367   std::string origArtist(artist.strArtist);
1368 
1369   bool stop(false);
1370   while (!stop)
1371   {
1372     stop = true;
1373     CLog::Log(LOGDEBUG, "%s downloading info for: %s", __FUNCTION__, artist.strArtist.c_str());
1374     artistDownloadStatus = DownloadArtistInfo(artist, scraper, artistInfo, !bAllowSelection, pDialog);
1375     if (artistDownloadStatus == INFO_NOT_FOUND)
1376     {
1377       if (pDialog && bAllowSelection)
1378       {
1379         if (!CGUIKeyboardFactory::ShowAndGetInput(artist.strArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
1380           artistDownloadStatus = INFO_CANCELLED;
1381         else
1382           stop = false;
1383       }
1384       else
1385       {
1386         CServiceBroker::GetEventLog().Add(EventPtr(
1387             new CMediaLibraryEvent(MediaTypeArtist, artist.strPath, 24146,
1388                                    StringUtils::Format(g_localizeStrings.Get(24147).c_str(),
1389                                                        MediaTypeArtist, artist.strArtist.c_str()),
1390                                    CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType()),
1391                                    CURL::GetRedacted(artist.strPath), EventLevel::Warning)));
1392       }
1393     }
1394   }
1395 
1396   // Restore original artist name, possibly changed by manual entry to get info
1397   // but should not change outside merge
1398   artist.strArtist = origArtist;
1399 
1400   if (artistDownloadStatus == INFO_ADDED)
1401   {
1402     artist.MergeScrapedArtist(artistInfo.GetArtist(), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS));
1403     m_musicDatabase.UpdateArtist(artist);
1404     artistInfo.SetLoaded();
1405   }
1406 
1407   if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1408           CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) ==
1409       CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
1410     return artistDownloadStatus;
1411 
1412   // Check artist art.
1413   // Fill any gaps with local art files, or use first available from scraped
1414   // list (when it has been successfuly scraped). Do this even when no info
1415   // added (cancelled, not found or error), there may be new local art files.
1416   // Get individual artist subfolder within the Artist Information Folder
1417   m_musicDatabase.GetArtistPath(artist, artist.strPath);
1418   // Location of local art
1419   std::string artfolder;
1420   if (CDirectory::Exists(artist.strPath))
1421     // When subfolder exists that is only place we look for art
1422     artfolder = artist.strPath;
1423   else
1424   {
1425     // Fallback to the old location local to music files (when there is a
1426     // unique folder). If there is no folder for the artist, and *only* the
1427     // artist, this will be blank
1428     m_musicDatabase.GetOldArtistPath(artist.idArtist, artfolder);
1429   }
1430   if (AddArtistArtwork(artist, artfolder))
1431     artistDownloadStatus = INFO_ADDED; // Local art added
1432 
1433   return artistDownloadStatus; // Added, cancelled or not found
1434 }
1435 
1436 #define THRESHOLD .95f
1437 
1438 CInfoScanner::INFO_RET
DownloadAlbumInfo(const CAlbum & album,const ADDON::ScraperPtr & info,CMusicAlbumInfo & albumInfo,bool bUseScrapedMBID,CGUIDialogProgress * pDialog)1439 CMusicInfoScanner::DownloadAlbumInfo(const CAlbum& album,
1440                                      const ADDON::ScraperPtr& info,
1441                                      CMusicAlbumInfo& albumInfo,
1442                                      bool bUseScrapedMBID,
1443                                      CGUIDialogProgress* pDialog)
1444 {
1445   if (m_handle)
1446   {
1447     m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20321).c_str(), info->Name().c_str()));
1448     m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
1449   }
1450 
1451   // clear our scraper cache
1452   info->ClearCache();
1453 
1454   CMusicInfoScraper scraper(info);
1455   bool bMusicBrainz = false;
1456   /*
1457   When the mbid is derived from tags scraping of album information is done directly
1458   using that ID, otherwise the lookup is based on album and artist names and can mis-identify the
1459   album (i.e. classical music has many "Symphony No. 5"). To be able to correct any mistakes a
1460   manual refresh of artist information uses either the mbid if derived from tags or the album
1461   and artist names, not any previously scraped mbid.
1462   */
1463   if (!album.strMusicBrainzAlbumID.empty() && (!album.bScrapedMBID || bUseScrapedMBID))
1464   {
1465     CScraperUrl musicBrainzURL;
1466     if (ResolveMusicBrainz(album.strMusicBrainzAlbumID, info, musicBrainzURL))
1467     {
1468       CMusicAlbumInfo albumNfo("nfo", musicBrainzURL);
1469       scraper.GetAlbums().clear();
1470       scraper.GetAlbums().push_back(albumNfo);
1471       bMusicBrainz = true;
1472     }
1473   }
1474 
1475   // handle nfo files
1476   bool existsNFO = false;
1477   std::string path = album.strPath;
1478   if (path.empty())
1479     m_musicDatabase.GetAlbumPath(album.idAlbum, path);
1480 
1481   std::string strNfo = URIUtils::AddFileToFolder(path, "album.nfo");
1482   CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
1483   CNfoFile nfoReader;
1484   existsNFO = XFILE::CFile::Exists(strNfo);
1485   // When on GUI ask user if they want to ignore nfo and refresh from Internet
1486   if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(10523, 20446))
1487   {
1488     existsNFO = false;
1489     CLog::Log(LOGDEBUG, "Ignoring nfo file: %s", CURL::GetRedacted(strNfo).c_str());
1490   }
1491   if (existsNFO)
1492   {
1493     CLog::Log(LOGDEBUG,"Found matching nfo file: %s", CURL::GetRedacted(strNfo).c_str());
1494     result = nfoReader.Create(strNfo, info);
1495     if (result == CInfoScanner::FULL_NFO)
1496     {
1497       CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
1498       nfoReader.GetDetails(albumInfo.GetAlbum());
1499       return INFO_ADDED;
1500     }
1501     else if (result == CInfoScanner::URL_NFO ||
1502              result == CInfoScanner::COMBINED_NFO)
1503     {
1504       CScraperUrl scrUrl(nfoReader.ScraperUrl());
1505       CMusicAlbumInfo albumNfo("nfo",scrUrl);
1506       ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
1507       CLog::Log(LOGDEBUG,"-- nfo-scraper: %s", nfoReaderScraper->Name().c_str());
1508       CLog::Log(LOGDEBUG, "-- nfo url: %s", scrUrl.GetFirstThumbUrl());
1509       scraper.SetScraperInfo(nfoReaderScraper);
1510       scraper.GetAlbums().clear();
1511       scraper.GetAlbums().push_back(albumNfo);
1512     }
1513     else if (result != CInfoScanner::OVERRIDE_NFO)
1514       CLog::Log(LOGERROR,"Unable to find an url in nfo file: %s", strNfo.c_str());
1515   }
1516 
1517   if (!scraper.CheckValidOrFallback(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER)))
1518   { // the current scraper is invalid, as is the default - bail
1519     CLog::Log(LOGERROR, "%s - current and default scrapers are invalid.  Pick another one", __FUNCTION__);
1520     return INFO_ERROR;
1521   }
1522 
1523   if (!scraper.GetAlbumCount())
1524   {
1525     scraper.FindAlbumInfo(album.strAlbum, album.GetAlbumArtistString());
1526 
1527     while (!scraper.Completed())
1528     {
1529       if (m_bStop)
1530       {
1531         scraper.Cancel();
1532         return INFO_CANCELLED;
1533       }
1534       ScannerWait(1);
1535     }
1536     /*
1537     Finding album using xml scraper may request data from Musicbrainz.
1538     MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1539     returns 503 errors for all calls from that IP address.
1540     To stay below the rate-limit threshold wait 1s before proceeding
1541     */
1542     if (!info->IsPython())
1543       ScannerWait(1000);
1544   }
1545 
1546   CGUIDialogSelect *pDlg = NULL;
1547   int iSelectedAlbum=0;
1548   if ((result == CInfoScanner::NO_NFO || result == CInfoScanner::OVERRIDE_NFO)
1549       && !bMusicBrainz)
1550   {
1551     iSelectedAlbum = -1; // set negative so that we can detect a failure
1552     if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1)
1553     {
1554       double bestRelevance = 0;
1555       double minRelevance = THRESHOLD;
1556       if (pDialog || scraper.GetAlbumCount() > 1) // score the matches
1557       {
1558         //show dialog with all albums found
1559         if (pDialog)
1560         {
1561           pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
1562           pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
1563           pDlg->Reset();
1564           pDlg->EnableButton(true, 413); // manual
1565           pDlg->SetUseDetails(true);
1566         }
1567 
1568         CFileItemList items;
1569         for (int i = 0; i < scraper.GetAlbumCount(); ++i)
1570         {
1571           CMusicAlbumInfo& info = scraper.GetAlbum(i);
1572           double relevance = info.GetRelevance();
1573           if (relevance < 0)
1574             relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, album.strAlbum,
1575                         info.GetAlbum().GetAlbumArtistString(),
1576                         album.GetAlbumArtistString());
1577 
1578           // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
1579           // otherwise, perfect matches only
1580           if (relevance >= std::max(minRelevance, bestRelevance))
1581           { // we auto-select the best of these
1582             bestRelevance = relevance;
1583             iSelectedAlbum = i;
1584           }
1585           if (pDialog)
1586           {
1587             // set the label to [relevance]  album - artist
1588             std::string strTemp = StringUtils::Format("[%0.2f]  %s", relevance, info.GetTitle2().c_str());
1589             CFileItemPtr item(new CFileItem("", false));
1590             item->SetLabel(strTemp);
1591 
1592             std::string strTemp2;
1593             if (!scraper.GetAlbum(i).GetAlbum().strType.empty())
1594               strTemp2 += scraper.GetAlbum(i).GetAlbum().strType;
1595             if (!scraper.GetAlbum(i).GetAlbum().strReleaseDate.empty())
1596               strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseDate;
1597             if (!scraper.GetAlbum(i).GetAlbum().strReleaseStatus.empty())
1598               strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseStatus;
1599             if (!scraper.GetAlbum(i).GetAlbum().strLabel.empty())
1600               strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strLabel;
1601             item->SetLabel2(strTemp2);
1602 
1603             item->SetArt(scraper.GetAlbum(i).GetAlbum().art);
1604 
1605             items.Add(item);
1606           }
1607           if (!pDialog && relevance > .999f) // we're so close, no reason to search further
1608             break;
1609         }
1610 
1611         if (pDialog)
1612         {
1613           pDlg->Sort(false);
1614           pDlg->SetItems(items);
1615           pDlg->Open();
1616 
1617           // and wait till user selects one
1618           if (pDlg->GetSelectedItem() < 0)
1619           { // none chosen
1620             if (!pDlg->IsButtonPressed())
1621               return INFO_CANCELLED;
1622 
1623             // manual button pressed
1624             std::string strNewAlbum = album.strAlbum;
1625             if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum, CVariant{g_localizeStrings.Get(16011)}, false))
1626               return INFO_CANCELLED;
1627             if (strNewAlbum == "")
1628               return INFO_CANCELLED;
1629 
1630             std::string strNewArtist = album.GetAlbumArtistString();
1631             if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
1632               return INFO_CANCELLED;
1633 
1634             pDialog->SetLine(0, CVariant{strNewAlbum});
1635             pDialog->SetLine(1, CVariant{strNewArtist});
1636             pDialog->Progress();
1637 
1638             CAlbum newAlbum = album;
1639             newAlbum.strAlbum = strNewAlbum;
1640             newAlbum.strArtistDesc = strNewArtist;
1641 
1642             return DownloadAlbumInfo(newAlbum, info, albumInfo, bUseScrapedMBID, pDialog);
1643           }
1644           iSelectedAlbum = pDlg->GetSelectedItem();
1645         }
1646       }
1647       else
1648       {
1649         CMusicAlbumInfo& info = scraper.GetAlbum(0);
1650         double relevance = info.GetRelevance();
1651         if (relevance < 0)
1652           relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum,
1653                                             album.strAlbum,
1654                                             info.GetAlbum().GetAlbumArtistString(),
1655                                             album.GetAlbumArtistString());
1656         if (relevance < THRESHOLD)
1657           return INFO_NOT_FOUND;
1658 
1659         iSelectedAlbum = 0;
1660       }
1661     }
1662 
1663     if (iSelectedAlbum < 0)
1664       return INFO_NOT_FOUND;
1665 
1666   }
1667 
1668   scraper.LoadAlbumInfo(iSelectedAlbum);
1669   while (!scraper.Completed())
1670   {
1671     if (m_bStop)
1672     {
1673       scraper.Cancel();
1674       return INFO_CANCELLED;
1675     }
1676     ScannerWait(1);
1677   }
1678   if (!scraper.Succeeded())
1679     return INFO_ERROR;
1680   /*
1681   Fetching album details using xml scraper may makes requests for data from Musicbrainz.
1682   MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1683   returns 503 errors for all calls from that IP address.
1684   To stay below the rate-limit threshold wait 1s before proceeding incase next action is
1685   to scrape another album or artist
1686   */
1687   if (!info->IsPython())
1688     ScannerWait(1000);
1689 
1690   albumInfo = scraper.GetAlbum(iSelectedAlbum);
1691 
1692   if (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
1693     nfoReader.GetDetails(albumInfo.GetAlbum(), NULL, true);
1694 
1695   return INFO_ADDED;
1696 }
1697 
1698 CInfoScanner::INFO_RET
DownloadArtistInfo(const CArtist & artist,const ADDON::ScraperPtr & info,MUSIC_GRABBER::CMusicArtistInfo & artistInfo,bool bUseScrapedMBID,CGUIDialogProgress * pDialog)1699 CMusicInfoScanner::DownloadArtistInfo(const CArtist& artist,
1700                                       const ADDON::ScraperPtr& info,
1701                                       MUSIC_GRABBER::CMusicArtistInfo& artistInfo,
1702                                       bool bUseScrapedMBID,
1703                                       CGUIDialogProgress* pDialog)
1704 {
1705   if (m_handle)
1706   {
1707     m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20320).c_str(), info->Name().c_str()));
1708     m_handle->SetText(artist.strArtist);
1709   }
1710 
1711   // clear our scraper cache
1712   info->ClearCache();
1713 
1714   CMusicInfoScraper scraper(info);
1715   bool bMusicBrainz = false;
1716   /*
1717   When the mbid is derived from tags scraping of artist information is done directly
1718   using that ID, otherwise the lookup is based on name and can mis-identify the artist
1719   (many have same name). To be able to correct any mistakes a manual refresh of artist
1720   information uses either the mbid if derived from tags or the artist name, not any previously
1721   scraped mbid.
1722   */
1723   if (!artist.strMusicBrainzArtistID.empty() && (!artist.bScrapedMBID || bUseScrapedMBID))
1724   {
1725     CScraperUrl musicBrainzURL;
1726     if (ResolveMusicBrainz(artist.strMusicBrainzArtistID, info, musicBrainzURL))
1727     {
1728       CMusicArtistInfo artistNfo("nfo", musicBrainzURL);
1729       scraper.GetArtists().clear();
1730       scraper.GetArtists().push_back(artistNfo);
1731       bMusicBrainz = true;
1732     }
1733   }
1734 
1735   // Handle nfo files
1736   CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
1737   CNfoFile nfoReader;
1738   std::string strNfo;
1739   std::string path;
1740   bool existsNFO = false;
1741   // First look for nfo in the artists folder, the primary location
1742   path = artist.strPath;
1743   // Get path when don't already have it.
1744   bool artistpathfound = !path.empty();
1745   if (!artistpathfound)
1746     artistpathfound = m_musicDatabase.GetArtistPath(artist, path);
1747   if (artistpathfound)
1748   {
1749     strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
1750     existsNFO = XFILE::CFile::Exists(strNfo);
1751   }
1752 
1753   // If not there fall back local to music files (historic location for those album artists with a unique folder)
1754   if (!existsNFO)
1755   {
1756     artistpathfound = m_musicDatabase.GetOldArtistPath(artist.idArtist, path);
1757     if (artistpathfound)
1758     {
1759       strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
1760       existsNFO = XFILE::CFile::Exists(strNfo);
1761     }
1762     else
1763       CLog::Log(LOGDEBUG, "%s not have path, nfo file not possible", artist.strArtist.c_str());
1764   }
1765 
1766   // When on GUI ask user if they want to ignore nfo and refresh from Internet
1767   if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(21891, 20446))
1768   {
1769     existsNFO = false;
1770     CLog::Log(LOGDEBUG, "Ignoring nfo file: %s", CURL::GetRedacted(strNfo).c_str());
1771   }
1772 
1773   if (existsNFO)
1774   {
1775     CLog::Log(LOGDEBUG, "Found matching nfo file: %s", CURL::GetRedacted(strNfo).c_str());
1776     result = nfoReader.Create(strNfo, info);
1777     if (result == CInfoScanner::FULL_NFO)
1778     {
1779       CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
1780       nfoReader.GetDetails(artistInfo.GetArtist());
1781       return INFO_ADDED;
1782     }
1783     else if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
1784     {
1785       CScraperUrl scrUrl(nfoReader.ScraperUrl());
1786       CMusicArtistInfo artistNfo("nfo", scrUrl);
1787       ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
1788       CLog::Log(LOGDEBUG, "-- nfo-scraper: %s", nfoReaderScraper->Name().c_str());
1789       CLog::Log(LOGDEBUG, "-- nfo url: %s", scrUrl.GetFirstThumbUrl());
1790       scraper.SetScraperInfo(nfoReaderScraper);
1791       scraper.GetArtists().push_back(artistNfo);
1792     }
1793     else
1794       CLog::Log(LOGERROR, "Unable to find an url in nfo file: %s", strNfo.c_str());
1795   }
1796 
1797   if (!scraper.GetArtistCount())
1798   {
1799     scraper.FindArtistInfo(artist.strArtist);
1800 
1801     while (!scraper.Completed())
1802     {
1803       if (m_bStop)
1804       {
1805         scraper.Cancel();
1806         return INFO_CANCELLED;
1807       }
1808       ScannerWait(1);
1809     }
1810     /*
1811     Finding artist using xml scraper makes a request for data from Musicbrainz.
1812     MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter
1813     the server returns 503 errors for all calls from that IP address. To stay
1814     below the rate-limit threshold wait 1s before proceeding
1815     */
1816     if (!info->IsPython())
1817       ScannerWait(1000);
1818   }
1819 
1820   int iSelectedArtist = 0;
1821   if (result == CInfoScanner::NO_NFO && !bMusicBrainz)
1822   {
1823     if (scraper.GetArtistCount() >= 1)
1824     {
1825       // now load the first match
1826       if (pDialog && scraper.GetArtistCount() > 1)
1827       {
1828         // if we found more then 1 album, let user choose one
1829         CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
1830         if (pDlg)
1831         {
1832           pDlg->SetHeading(CVariant{g_localizeStrings.Get(21890)});
1833           pDlg->Reset();
1834           pDlg->EnableButton(true, 413); // manual
1835 
1836           for (int i = 0; i < scraper.GetArtistCount(); ++i)
1837           {
1838             // set the label to artist
1839             CFileItem item(scraper.GetArtist(i).GetArtist());
1840             std::string strTemp = scraper.GetArtist(i).GetArtist().strArtist;
1841             if (!scraper.GetArtist(i).GetArtist().strBorn.empty())
1842               strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")";
1843             if (!scraper.GetArtist(i).GetArtist().strDisambiguation.empty())
1844               strTemp += " - " + scraper.GetArtist(i).GetArtist().strDisambiguation;
1845             if (!scraper.GetArtist(i).GetArtist().genre.empty())
1846             {
1847               std::string genres = StringUtils::Join(scraper.GetArtist(i).GetArtist().genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
1848               if (!genres.empty())
1849                 strTemp = StringUtils::Format("[%s] %s", genres.c_str(), strTemp.c_str());
1850             }
1851             item.SetLabel(strTemp);
1852             item.m_idepth = i; // use this to hold the index of the album in the scraper
1853             pDlg->Add(item);
1854           }
1855           pDlg->Open();
1856 
1857           // and wait till user selects one
1858           if (pDlg->GetSelectedItem() < 0)
1859           { // none chosen
1860             if (!pDlg->IsButtonPressed())
1861               return INFO_CANCELLED;
1862 
1863             // manual button pressed
1864             std::string strNewArtist = artist.strArtist;
1865             if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
1866               return INFO_CANCELLED;
1867 
1868             if (pDialog)
1869             {
1870               pDialog->SetLine(0, CVariant{strNewArtist});
1871               pDialog->Progress();
1872             }
1873 
1874             CArtist newArtist;
1875             newArtist.strArtist = strNewArtist;
1876             return DownloadArtistInfo(newArtist, info, artistInfo, bUseScrapedMBID, pDialog);
1877           }
1878           iSelectedArtist = pDlg->GetSelectedFileItem()->m_idepth;
1879         }
1880       }
1881     }
1882     else
1883       return INFO_NOT_FOUND;
1884   }
1885   /*
1886   Fetching artist details using xml scraper makes requests for data from Musicbrainz.
1887   MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1888   returns 503 errors for all calls from that IP address.
1889   To stay below the rate-limit threshold wait 1s before proceeding incase next action is
1890   to scrape another album or artist
1891   */
1892   if (!info->IsPython())
1893     ScannerWait(1000);
1894 
1895   scraper.LoadArtistInfo(iSelectedArtist, artist.strArtist);
1896   while (!scraper.Completed())
1897   {
1898     if (m_bStop)
1899     {
1900       scraper.Cancel();
1901       return INFO_CANCELLED;
1902     }
1903     ScannerWait(1);
1904   }
1905 
1906   if (!scraper.Succeeded())
1907     return INFO_ERROR;
1908 
1909   artistInfo = scraper.GetArtist(iSelectedArtist);
1910 
1911   if (result == CInfoScanner::COMBINED_NFO)
1912     nfoReader.GetDetails(artistInfo.GetArtist(), NULL, true);
1913 
1914   return INFO_ADDED;
1915 }
1916 
ResolveMusicBrainz(const std::string & strMusicBrainzID,const ScraperPtr & preferredScraper,CScraperUrl & musicBrainzURL)1917 bool CMusicInfoScanner::ResolveMusicBrainz(const std::string &strMusicBrainzID, const ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL)
1918 {
1919   // We have a MusicBrainz ID
1920   // Get a scraper that can resolve it to a MusicBrainz URL & force our
1921   // search directly to the specific album.
1922   bool bMusicBrainz = false;
1923   try
1924   {
1925     musicBrainzURL = preferredScraper->ResolveIDToUrl(strMusicBrainzID);
1926   }
1927   catch (const ADDON::CScraperError &sce)
1928   {
1929     if (sce.FAborted())
1930       return false;
1931   }
1932 
1933   if (musicBrainzURL.HasUrls())
1934   {
1935     CLog::Log(LOGDEBUG,"-- nfo-scraper: %s",preferredScraper->Name().c_str());
1936     CLog::Log(LOGDEBUG, "-- nfo url: %s", musicBrainzURL.GetFirstThumbUrl());
1937     bMusicBrainz = true;
1938   }
1939 
1940   return bMusicBrainz;
1941 }
1942 
ScannerWait(unsigned int milliseconds)1943 void CMusicInfoScanner::ScannerWait(unsigned int milliseconds)
1944 {
1945   if (milliseconds > 10)
1946   {
1947     CEvent m_StopEvent;
1948     m_StopEvent.WaitMSec(milliseconds);
1949   }
1950   else
1951     std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
1952 }
1953 
AddArtistArtwork(CArtist & artist,const std::string & artfolder)1954 bool CMusicInfoScanner::AddArtistArtwork(CArtist& artist, const std::string& artfolder)
1955 {
1956   if (!artist.thumbURL.HasUrls() && artfolder.empty())
1957     return false; // No local or scraped possible art to process
1958 
1959   if (artist.art.empty())
1960     m_musicDatabase.GetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
1961 
1962   std::map<std::string, std::string> addedart;
1963   std::string strArt;
1964 
1965   // Handle thumb separately, can be from multiple confgurable file names
1966   if (artist.art.find("thumb") == artist.art.end())
1967   {
1968     if (!artfolder.empty())
1969     { // Local music thumbnail images named by "musiclibrary.musicthumbs"
1970       CFileItem item(artfolder, true);
1971       strArt = item.GetUserMusicThumb(true);
1972     }
1973     if (strArt.empty())
1974       strArt = CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType("thumb"));
1975     if (!strArt.empty())
1976       addedart.insert(std::make_pair("thumb", strArt));
1977   }
1978 
1979   // Process additional art types in artist folder
1980   AddLocalArtwork(addedart, MediaTypeArtist, artist.strArtist, artfolder);
1981 
1982   // Process remote artist art filling gaps with first of scraped art URLs
1983   AddRemoteArtwork(addedart, MediaTypeArtist, artist.thumbURL);
1984 
1985   int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1986       CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
1987 
1988   for (const auto& it : addedart)
1989   {
1990     // Cache thumb, fanart and other whitelisted artwork immediately
1991     // (other art types will be cached when first displayed)
1992     if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
1993         it.first == "fanart")
1994       CTextureCache::GetInstance().BackgroundCacheImage(it.second);
1995     auto ret = artist.art.insert(it);
1996     if (ret.second)
1997       m_musicDatabase.SetArtForItem(artist.idArtist, MediaTypeArtist, it.first, it.second);
1998   }
1999   return addedart.size() > 0;
2000 }
2001 
AddAlbumArtwork(CAlbum & album)2002 bool CMusicInfoScanner::AddAlbumArtwork(CAlbum& album)
2003 {
2004   // Fetch album path and any subfolders (disc sets).
2005   // No paths found when songs from different albums are in one folder
2006   std::vector<std::pair<std::string, int>> paths;
2007   m_musicDatabase.GetAlbumPaths(album.idAlbum, paths);
2008   for (const auto& pathpair : paths)
2009   {
2010     if (album.strPath.empty())
2011       album.strPath = pathpair.first.c_str();
2012     else
2013       // When more than one album path is the common path
2014       URIUtils::GetCommonPath(album.strPath, pathpair.first.c_str());
2015   }
2016 
2017   if (!album.thumbURL.HasUrls() && album.strPath.empty())
2018     return false; // No local or scraped possible art to process
2019 
2020   if (album.art.empty())
2021     m_musicDatabase.GetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
2022   auto thumb = album.art.find("thumb"); // Find "thumb", may want to replace it
2023 
2024   bool replaceThumb = paths.size() > 1;
2025   if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2026           CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART))
2027   {
2028     // When "prefer online album art" enabled and we have a thumb as embedded art
2029     // then replace it if we find a scraped cover
2030     if (thumb != album.art.end() && StringUtils::StartsWith(thumb->second, "image://"))
2031       replaceThumb = true;
2032   }
2033 
2034   std::map<std::string, std::string> addedart;
2035   std::string strArt;
2036 
2037   // Fetch local art from album folder
2038   // Handle thumbs separately, can be from multiple confgurable file names
2039   if (replaceThumb || thumb == album.art.end())
2040   {
2041     if (!album.strPath.empty())
2042     { // Local music thumbnail images named by "musiclibrary.musicthumbs"
2043       CFileItem item(album.strPath, true);
2044       strArt = item.GetUserMusicThumb(true);
2045     }
2046     if (strArt.empty())
2047       strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType("thumb"));
2048     if (!strArt.empty())
2049     {
2050       if (thumb != album.art.end())
2051         album.art.erase(thumb);
2052       addedart.insert(std::make_pair("thumb", strArt));
2053     }
2054   }
2055   // Process additional art types in album folder
2056   AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, album.strPath);
2057 
2058   // Fetch local art from disc subfolders
2059   if (paths.size() > 1)
2060   {
2061     CMusicThumbLoader loader;
2062     std::string firstDiscThumb;
2063     int iDiscThumb = 10000;
2064     for (const auto& pathpair : paths)
2065     {
2066       strArt.clear();
2067 
2068       int discnum = m_musicDatabase.GetDiscnumberForPathID(pathpair.second);
2069       if (discnum > 0)
2070       {
2071         // Handle thumbs separately. Get thumb for path from textures db cached during scan
2072         // (could be embedded or local file from multiple confgurable file names)
2073         CFileItem item(pathpair.first.c_str(), true);
2074         std::string strArtType = StringUtils::Format("%s%i", "thumb", discnum);
2075         strArt = loader.GetCachedImage(item, "thumb");
2076         if (strArt.empty())
2077           strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType(strArtType));
2078         if (!strArt.empty())
2079         {
2080           addedart.insert(std::make_pair(strArtType, strArt));
2081           // Store thumb of first disc with a thumb
2082           if (discnum < iDiscThumb)
2083           {
2084             iDiscThumb = discnum;
2085             firstDiscThumb = strArt;
2086           }
2087         }
2088       }
2089       // Process additional art types in disc subfolder
2090       AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, pathpair.first, discnum);
2091     }
2092     // Finally if we still don't have album thumb then use the art from the
2093     // first disc in the set with a thumb
2094     if (!firstDiscThumb.empty() && album.art.find("thumb") == album.art.end())
2095     {
2096       m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, "thumb", firstDiscThumb);
2097       // Assign art as folder thumb (in textures db) as well
2098 
2099       CFileItem albumItem(album.strPath, true);
2100       loader.SetCachedImage(albumItem, "thumb", firstDiscThumb);
2101     }
2102   }
2103 
2104   // Process remote album art filling gaps with first of scraped art URLs
2105   AddRemoteArtwork(addedart, MediaTypeAlbum, album.thumbURL);
2106 
2107   int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2108       CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
2109   for (const auto& it : addedart)
2110   {
2111     // Cache thumb, fanart and whitelisted artwork immediately
2112     // (other art types will be cached when first displayed)
2113     if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
2114         it.first == "fanart")
2115       CTextureCache::GetInstance().BackgroundCacheImage(it.second);
2116 
2117     auto ret = album.art.insert(it);
2118     if (ret.second)
2119       m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, it.first, it.second);
2120   }
2121   return addedart.size() > 0;
2122 }
2123 
GetArtWhitelist(const MediaType & mediaType,int iArtLevel)2124 std::vector<CVariant> CMusicInfoScanner::GetArtWhitelist(const MediaType& mediaType, int iArtLevel)
2125 {
2126   std::vector<CVariant> whitelistarttypes;
2127   if (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
2128   {
2129     // Basic artist artwork = thumb + fanart (but not "family" fanart1, fanart2 etc.)
2130     // Basic album artwork = thumb only, thumb handled separately not in whitelist
2131     if (mediaType == MediaTypeArtist)
2132       whitelistarttypes.emplace_back("fanart");
2133   }
2134   else
2135   {
2136     if (mediaType == MediaTypeArtist)
2137       whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2138           CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST);
2139     else
2140       whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2141           CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST);
2142   }
2143 
2144   return whitelistarttypes;
2145 }
2146 
AddLocalArtwork(std::map<std::string,std::string> & art,const std::string & mediaType,const std::string & mediaName,const std::string & artfolder,int discnum)2147 bool CMusicInfoScanner::AddLocalArtwork(std::map<std::string, std::string>& art,
2148                                         const std::string& mediaType,
2149                                         const std::string& mediaName,
2150                                         const std::string& artfolder,
2151                                         int discnum)
2152 {
2153   if (artfolder.empty())
2154     return false;
2155 
2156   int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2157       CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
2158 
2159   std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
2160   bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
2161                  ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
2162                   CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2163                       CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART));
2164 
2165   // Not useall and empty whiltelist means no extra art is picked up from either place
2166   if (!bUseAll && whitelistarttypes.empty())
2167     return false;
2168 
2169   // Image files used as thumbs
2170   std::vector<CVariant> thumbs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2171       CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS);
2172 
2173   // Find local art
2174   CFileItemList availableArtFiles;
2175   CDirectory::GetDirectory(artfolder, availableArtFiles,
2176                            CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
2177                            DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
2178 
2179   for (const auto& artFile : availableArtFiles)
2180   {
2181     if (artFile->m_bIsFolder)
2182       continue;
2183     std::string strCandidate = URIUtils::GetFileName(artFile->GetPath());
2184     // Strip media name
2185     if (!mediaName.empty() && StringUtils::StartsWith(strCandidate, mediaName))
2186       strCandidate.erase(0, mediaName.length());
2187     StringUtils::ToLower(strCandidate);
2188     // Skip files already used as "thumb"
2189     // Typically folder.jpg but can be from multiple confgurable file names
2190     if (std::find(thumbs.begin(), thumbs.end(), strCandidate) != thumbs.end())
2191       continue;
2192     // Grab and strip file extension
2193     std::string strExt;
2194     size_t period = strCandidate.find_last_of("./\\");
2195     if (period != std::string::npos && strCandidate[period] == '.')
2196     {
2197       strExt = strCandidate.substr(period); // ".jpg", ".png" etc.
2198       strCandidate.erase(period); // "abc16" for file Abc16.jpg
2199     }
2200     if (strCandidate.empty())
2201       continue;
2202     // Validate art type name
2203     size_t last_index = strCandidate.find_last_not_of("0123456789");
2204     std::string strDigits = strCandidate.substr(last_index + 1);
2205     std::string strFamily = strCandidate.substr(0, last_index + 1); // "abc" of "abc16"
2206     if (strFamily.empty())
2207       continue;
2208     if (!MUSIC_UTILS::IsValidArtType(strCandidate))
2209       continue;
2210     // Disc specific art from disc subfolder
2211     // Skip art where digits of filename do not match disc number
2212     if (discnum > 0 && !strDigits.empty() && (atoi(strDigits.c_str()) != discnum))
2213       continue;
2214 
2215     // Use all art, or check for basic level art in whitelist exactly allowing for disc number,
2216     // or for custom art check whitelist contains art type family (strip trailing digits)
2217     // e.g. 'fanart', 'fanart1', 'fanart2' etc. all match whitelist entry 'fanart'
2218     std::string strCheck = strCandidate;
2219     if (discnum > 0 || iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM)
2220       strCheck = strFamily;
2221     if (bUseAll || std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strCheck) !=
2222                        whitelistarttypes.end())
2223     {
2224       if (!strDigits.empty())
2225       {
2226         // Catch any variants of music thumbs e.g. folder2.jpg as "thumb2"
2227         // Used for disc sets when files all in one album folder
2228         if (std::find(thumbs.begin(), thumbs.end(), strFamily + strExt) != thumbs.end())
2229           strCandidate = "thumb" + strDigits;
2230       }
2231       else if (discnum > 0)
2232         // Append disc number when candidate art type (and file) not have it
2233         strCandidate += StringUtils::Format("%i", discnum);
2234 
2235       if (art.find(strCandidate) == art.end())
2236         art.insert(std::make_pair(strCandidate, artFile->GetPath()));
2237     }
2238   }
2239 
2240   return art.size() > 0;
2241 }
2242 
AddRemoteArtwork(std::map<std::string,std::string> & art,const std::string & mediaType,const CScraperUrl & thumbURL)2243 bool CMusicInfoScanner::AddRemoteArtwork(std::map<std::string, std::string>& art,
2244                                          const std::string& mediaType,
2245                                          const CScraperUrl& thumbURL)
2246 {
2247   int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2248       CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
2249 
2250   std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
2251   bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
2252                  ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
2253                   CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2254                       CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART));
2255 
2256   // not useall and empty whiltelist means no extra art is picked up from either place
2257   if (!bUseAll && whitelistarttypes.empty())
2258     return false;
2259 
2260   // Add online art
2261   // Done for artists and albums, so not need repeating at disc level
2262   for (const auto& url : thumbURL.GetUrls())
2263   {
2264     // Art type is encoded into the scraper XML held in thumbURL as optional "aspect=" field.
2265     // Those URL without aspect are also returned for all other type values.
2266     // Loop through all the first URLS of each type except "thumb" and add if art missing
2267     if (url.m_aspect.empty() || url.m_aspect == "thumb")
2268       continue;
2269     if (!bUseAll)
2270     { // Check whitelist for art type family e.g. "discart" for aspect="discart2"
2271       std::string strName = url.m_aspect;
2272       if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
2273         StringUtils::TrimRight(strName, "0123456789");
2274       if (std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strName) ==
2275           whitelistarttypes.end())
2276         continue;
2277     }
2278     if (art.find(url.m_aspect) == art.end())
2279     {
2280       std::string strArt = CScraperUrl::GetThumbUrl(url);
2281       if (!strArt.empty())
2282         art.insert(std::make_pair(url.m_aspect, strArt));
2283     }
2284   }
2285 
2286   return art.size() > 0;
2287 }
2288 
2289 // This function is the Run() function of the IRunnable
2290 // CFileCountReader and runs in a separate thread.
Run()2291 void CMusicInfoScanner::Run()
2292 {
2293   int count = 0;
2294   for (auto& it : m_pathsToScan)
2295   {
2296     count += CountFilesRecursively(it);
2297   }
2298   m_itemCount = count;
2299 }
2300 
2301 // Recurse through all folders we scan and count files
CountFilesRecursively(const std::string & strPath)2302 int CMusicInfoScanner::CountFilesRecursively(const std::string& strPath)
2303 {
2304   // load subfolder
2305   CFileItemList items;
2306   CDirectory::GetDirectory(strPath, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), DIR_FLAG_NO_FILE_DIRS);
2307 
2308   if (m_bStop)
2309     return 0;
2310 
2311   // true for recursive counting
2312   int count = CountFiles(items, true);
2313   return count;
2314 }
2315 
CountFiles(const CFileItemList & items,bool recursive)2316 int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive)
2317 {
2318   int count = 0;
2319   for (int i=0; i<items.Size(); ++i)
2320   {
2321     const CFileItemPtr pItem=items[i];
2322 
2323     if (recursive && pItem->m_bIsFolder)
2324       count+=CountFilesRecursively(pItem->GetPath());
2325     else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
2326       count++;
2327   }
2328   return count;
2329 }
2330