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> ®exps = 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