2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "MusicInfoScanner.h"
12 #include "FileItemList.h"
13 #include "GUIInfoManager.h"
14 #include "GUIUserMessages.h"
15 #include "MusicAlbumInfo.h"
16 #include "MusicInfoScraper.h"
18 #include "ServiceBroker.h"
19 #include "TextureCache.h"
22 #include "addons/AddonSystemSettings.h"
23 #include "addons/Scraper.h"
24 #include "addons/addoninfo/AddonType.h"
25 #include "dialogs/GUIDialogExtendedProgressBar.h"
26 #include "dialogs/GUIDialogProgress.h"
27 #include "dialogs/GUIDialogSelect.h"
28 #include "dialogs/GUIDialogYesNo.h"
29 #include "events/EventLog.h"
30 #include "events/MediaLibraryEvent.h"
31 #include "filesystem/Directory.h"
32 #include "filesystem/MusicDatabaseDirectory.h"
33 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
34 #include "filesystem/SmartPlaylistDirectory.h"
35 #include "guilib/GUIComponent.h"
36 #include "guilib/GUIKeyboardFactory.h"
37 #include "guilib/GUIWindowManager.h"
38 #include "guilib/LocalizeStrings.h"
39 #include "interfaces/AnnouncementManager.h"
40 #include "music/MusicFileItemClassify.h"
41 #include "music/MusicLibraryQueue.h"
42 #include "music/MusicThumbLoader.h"
43 #include "music/MusicUtils.h"
44 #include "music/tags/MusicInfoTag.h"
45 #include "music/tags/MusicInfoTagLoaderFactory.h"
46 #include "settings/AdvancedSettings.h"
47 #include "settings/Settings.h"
48 #include "settings/SettingsComponent.h"
49 #include "utils/Digest.h"
50 #include "utils/FileExtensionProvider.h"
51 #include "utils/FileUtils.h"
52 #include "utils/StringUtils.h"
53 #include "utils/URIUtils.h"
54 #include "utils/Variant.h"
55 #include "utils/log.h"
61 using namespace MUSIC_INFO
;
62 using namespace XFILE
;
63 using namespace MUSICDATABASEDIRECTORY
;
64 using namespace MUSIC_GRABBER
;
65 using namespace ADDON
;
66 using KODI::UTILITY::CDigest
;
68 CMusicInfoScanner::CMusicInfoScanner()
69 : m_fileCountReader(this, "MusicFileCounter")
77 CMusicInfoScanner::~CMusicInfoScanner() = default;
79 void CMusicInfoScanner::Process()
82 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnScanStarted");
85 if (m_showDialog
&& !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE
))
87 CGUIDialogExtendedProgressBar
* dialog
=
88 CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogExtendedProgressBar
>(WINDOW_DIALOG_EXT_PROGRESS
);
90 m_handle
= dialog
->GetHandle(g_localizeStrings
.Get(314));
93 // check if we only need to perform a cleaning
94 if (m_bClean
&& m_pathsToScan
.empty())
96 CMusicLibraryQueue::GetInstance().CleanLibrary(false);
103 auto tick
= std::chrono::steady_clock::now();
104 m_musicDatabase
.Open();
105 m_bCanInterrupt
= true;
107 if (m_scanType
== 0) // load info from files
109 CLog::Log(LOGDEBUG
, "{} - Starting scan", __FUNCTION__
);
112 m_handle
->SetTitle(g_localizeStrings
.Get(505));
114 // Reset progress vars
118 // Create the thread to count all files to be scanned
120 m_fileCountReader
.Create();
122 // Database operations should not be canceled
123 // using Interrupt() while scanning as it could
124 // result in unexpected behaviour.
125 m_bCanInterrupt
= false;
126 m_needsCleanup
= false;
129 for (const auto& it
: m_pathsToScan
)
131 if (!CDirectory::Exists(it
) && !m_bClean
)
134 * Note that this will skip scanning (if m_bClean is disabled) if the directory really
135 * doesn't exist. Since the music scanner is fed with a list of existing paths from the DB
136 * and cleans out all songs under that path as its first step before re-adding files, if
137 * the entire source is offline we totally empty the music database in one go.
139 CLog::Log(LOGWARNING
, "{} directory '{}' does not exist - skipping scan.", __FUNCTION__
,
141 m_seenPaths
.insert(it
);
145 // Clear list of albums added by this scan
146 m_albumsAdded
.clear();
147 bool scancomplete
= DoScan(it
);
150 if (m_albumsAdded
.size() > 0)
152 // Set local art for added album disc sets and primary album artists
153 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
154 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
) !=
155 CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE
)
158 if (m_flags
& SCAN_ONLINE
)
159 // Download additional album and artist information for the recently added albums.
160 // This also identifies any local artist art if it exists, and gives it priority,
161 // otherwise it is set to the first available from the remote art that was scraped.
162 ScrapeInfoAddedAlbums();
174 CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
180 m_handle
->SetTitle(g_localizeStrings
.Get(700));
181 m_handle
->SetText("");
184 m_musicDatabase
.CleanupOrphanedItems();
185 m_musicDatabase
.CheckArtistLinksChanged();
188 m_handle
->SetTitle(g_localizeStrings
.Get(331));
190 m_musicDatabase
.Compress(false);
194 m_fileCountReader
.StopThread();
196 m_musicDatabase
.EmptyCache();
199 std::chrono::duration_cast
<std::chrono::seconds
>(std::chrono::steady_clock::now() - tick
);
201 "My Music: Scanning for music info using worker thread, operation took {}s",
204 if (m_scanType
== 1) // load album info
206 for (std::set
<std::string
>::const_iterator it
= m_pathsToScan
.begin(); it
!= m_pathsToScan
.end(); ++it
)
209 CDirectoryNode::GetDatabaseInfo(*it
, params
);
210 // Only scrape information for albums that have not been scraped before
211 // For refresh of information the lastscraped date is optionally clearered elsewhere
212 if (m_musicDatabase
.HasAlbumBeenScraped(params
.GetAlbumId()))
216 m_musicDatabase
.GetAlbum(params
.GetAlbumId(), album
);
219 float percentage
= static_cast<float>(std::distance(m_pathsToScan
.begin(), it
) * 100) / static_cast<float>(m_pathsToScan
.size());
220 m_handle
->SetText(album
.GetAlbumArtistString() + " - " + album
.strAlbum
);
221 m_handle
->SetPercentage(percentage
);
225 ADDON::ScraperPtr scraper
;
226 if (!m_musicDatabase
.GetScraper(album
.idAlbum
, CONTENT_ALBUMS
, scraper
))
229 UpdateDatabaseAlbumInfo(album
, scraper
, false);
235 if (m_scanType
== 2) // load artist info
237 for (std::set
<std::string
>::const_iterator it
= m_pathsToScan
.begin(); it
!= m_pathsToScan
.end(); ++it
)
240 CDirectoryNode::GetDatabaseInfo(*it
, params
);
241 // Only scrape information for artists that have not been scraped before
242 // For refresh of information the lastscraped date is optionally clearered elsewhere
243 if (m_musicDatabase
.HasArtistBeenScraped(params
.GetArtistId()))
247 m_musicDatabase
.GetArtist(params
.GetArtistId(), artist
);
248 m_musicDatabase
.GetArtistPath(artist
, artist
.strPath
);
252 float percentage
= static_cast<float>(std::distance(m_pathsToScan
.begin(), it
) * 100) / static_cast<float>(m_pathsToScan
.size());
253 m_handle
->SetText(artist
.strArtist
);
254 m_handle
->SetPercentage(percentage
);
258 ADDON::ScraperPtr scraper
;
259 if (!m_musicDatabase
.GetScraper(artist
.idArtist
, CONTENT_ARTISTS
, scraper
) || !scraper
)
262 UpdateDatabaseArtistInfo(artist
, scraper
, false);
268 //propagate artist sort names to albums and songs
269 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryArtistSortOnUpdate
)
270 m_musicDatabase
.UpdateArtistSortNames();
274 CLog::Log(LOGERROR
, "MusicInfoScanner: Exception while scanning.");
276 m_musicDatabase
.Close();
277 CLog::Log(LOGDEBUG
, "{} - Finished scan", __FUNCTION__
);
280 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnScanFinished");
282 // we need to clear the musicdb cache and update any active lists
283 CUtil::DeleteMusicDatabaseDirectoryCache();
284 CGUIMessage
msg(GUI_MSG_SCAN_FINISHED
, 0, 0, 0);
285 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
288 m_handle
->MarkFinished();
292 void CMusicInfoScanner::Start(const std::string
& strDirectory
, int flags
)
294 m_fileCountReader
.StopThread();
296 m_pathsToScan
.clear();
298 m_albumsAdded
.clear();
301 m_musicDatabase
.Open();
302 // Check db sources match xml file and update if they don't
303 m_musicDatabase
.UpdateSources();
305 if (strDirectory
.empty())
306 { // Scan all paths in the database. We do this by scanning all paths in the
307 // db, and crossing them off the list as we go.
308 m_musicDatabase
.GetPaths(m_pathsToScan
);
313 m_pathsToScan
.insert(strDirectory
);
314 m_idSourcePath
= m_musicDatabase
.GetSourceFromPath(strDirectory
);
316 m_musicDatabase
.Close();
318 m_bClean
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryCleanOnUpdate
;
325 void CMusicInfoScanner::FetchAlbumInfo(const std::string
& strDirectory
,
328 m_fileCountReader
.StopThread();
329 m_pathsToScan
.clear();
332 if (strDirectory
.empty())
334 m_musicDatabase
.Open();
335 m_musicDatabase
.GetAlbumsNav("musicdb://albums/", items
);
336 m_musicDatabase
.Close();
340 CURL
pathToUrl(strDirectory
);
342 if (pathToUrl
.IsProtocol("musicdb"))
345 CDirectoryNode::GetDatabaseInfo(strDirectory
, params
);
346 if (params
.GetAlbumId() != -1)
348 //Add single album (id and path) as item to scan
349 CFileItemPtr
item(new CFileItem(strDirectory
, false));
350 item
->GetMusicInfoTag()->SetDatabaseId(params
.GetAlbumId(), MediaTypeAlbum
);
355 CMusicDatabaseDirectory dir
;
356 NODE_TYPE childtype
= dir
.GetDirectoryChildType(strDirectory
);
357 if (childtype
== NODE_TYPE_ALBUM
)
358 dir
.GetDirectory(pathToUrl
, items
);
361 else if (StringUtils::EndsWith(strDirectory
, ".xsp"))
363 CSmartPlaylistDirectory dir
;
364 dir
.GetDirectory(pathToUrl
, items
);
368 m_musicDatabase
.Open();
369 for (int i
=0;i
<items
.Size();++i
)
371 if (CMusicDatabaseDirectory::IsAllItem(items
[i
]->GetPath()) || items
[i
]->IsParentFolder())
374 m_pathsToScan
.insert(items
[i
]->GetPath());
377 m_musicDatabase
.ClearAlbumLastScrapedTime(items
[i
]->GetMusicInfoTag()->GetDatabaseId());
380 m_musicDatabase
.Close();
387 void CMusicInfoScanner::FetchArtistInfo(const std::string
& strDirectory
,
390 m_fileCountReader
.StopThread();
391 m_pathsToScan
.clear();
394 if (strDirectory
.empty())
396 m_musicDatabase
.Open();
397 m_musicDatabase
.GetArtistsNav("musicdb://artists/", items
, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS
), -1);
398 m_musicDatabase
.Close();
402 CURL
pathToUrl(strDirectory
);
404 if (pathToUrl
.IsProtocol("musicdb"))
407 CDirectoryNode::GetDatabaseInfo(strDirectory
, params
);
408 if (params
.GetArtistId() != -1)
410 //Add single artist (id and path) as item to scan
411 CFileItemPtr
item(new CFileItem(strDirectory
, false));
412 item
->GetMusicInfoTag()->SetDatabaseId(params
.GetAlbumId(), MediaTypeArtist
);
417 CMusicDatabaseDirectory dir
;
418 NODE_TYPE childtype
= dir
.GetDirectoryChildType(strDirectory
);
419 if (childtype
== NODE_TYPE_ARTIST
)
420 dir
.GetDirectory(pathToUrl
, items
);
423 else if (StringUtils::EndsWith(strDirectory
, ".xsp"))
425 CSmartPlaylistDirectory dir
;
426 dir
.GetDirectory(pathToUrl
, items
);
430 m_musicDatabase
.Open();
431 for (int i
=0;i
<items
.Size();++i
)
433 if (CMusicDatabaseDirectory::IsAllItem(items
[i
]->GetPath()) || items
[i
]->IsParentFolder())
436 m_pathsToScan
.insert(items
[i
]->GetPath());
439 m_musicDatabase
.ClearArtistLastScrapedTime(items
[i
]->GetMusicInfoTag()->GetDatabaseId());
442 m_musicDatabase
.Close();
449 void CMusicInfoScanner::Stop()
452 m_musicDatabase
.Interrupt();
457 static void OnDirectoryScanned(const std::string
& strDirectory
)
459 CGUIMessage
msg(GUI_MSG_DIRECTORY_SCANNED
, 0, 0, 0);
460 msg
.SetStringParam(strDirectory
);
461 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
464 static std::string
Prettify(const std::string
& strDirectory
)
466 CURL
url(strDirectory
);
468 return CURL::Decode(url
.GetWithoutUserDetails());
471 bool CMusicInfoScanner::DoScan(const std::string
& strDirectory
)
475 m_handle
->SetTitle(g_localizeStrings
.Get(506)); //"Checking media files..."
476 m_handle
->SetText(Prettify(strDirectory
));
479 std::set
<std::string
>::const_iterator it
= m_seenPaths
.find(strDirectory
);
480 if (it
!= m_seenPaths
.end())
483 m_seenPaths
.insert(strDirectory
);
485 // Discard all excluded files defined by m_musicExcludeRegExps
486 const std::vector
<std::string
> ®exps
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps
;
488 if (CUtil::ExcludeFileOrFolder(strDirectory
, regexps
))
491 if (HasNoMedia(strDirectory
))
496 CDirectory::GetDirectory(strDirectory
, items
, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|.jpg|.tbn|.lrc|.cdg", DIR_FLAG_DEFAULTS
);
498 // sort and get the path hash. Note that we don't filter .cue sheet items here as we want
499 // to detect changes in the .cue sheet as well. The .cue sheet items only need filtering
500 // if we have a changed hash.
501 items
.Sort(SortByLabel
, SortOrderAscending
);
503 GetPathHash(items
, hash
);
505 // check whether we need to rescan or not
507 if ((m_flags
& SCAN_RESCAN
) || !m_musicDatabase
.GetPathHash(strDirectory
, dbHash
) || !StringUtils::EqualsNoCase(dbHash
, hash
))
508 { // path has changed - rescan
510 CLog::Log(LOGDEBUG
, "{} Scanning dir '{}' as not in the database", __FUNCTION__
,
511 CURL::GetRedacted(strDirectory
));
513 CLog::Log(LOGDEBUG
, "{} Rescanning dir '{}' due to change", __FUNCTION__
,
514 CURL::GetRedacted(strDirectory
));
517 m_handle
->SetTitle(g_localizeStrings
.Get(505)); //"Loading media information from files..."
519 // filter items in the sub dir (for .cue sheet support)
520 items
.FilterCueItems();
521 items
.Sort(SortByLabel
, SortOrderAscending
);
523 // and then scan in the new information from tags
524 if (RetrieveMusicInfo(strDirectory
, items
) > 0)
527 OnDirectoryScanned(strDirectory
);
530 // save information about this folder
531 m_musicDatabase
.SetPathHash(strDirectory
, hash
);
534 { // path is the same - no need to rescan
535 CLog::Log(LOGDEBUG
, "{} Skipping dir '{}' due to no change", __FUNCTION__
,
536 CURL::GetRedacted(strDirectory
));
537 m_currentItem
+= CountFiles(items
, false); // false for non-recursive
539 // updated the dialog with our progress
543 m_handle
->SetPercentage(static_cast<float>(m_currentItem
* 100) / static_cast<float>(m_itemCount
));
544 OnDirectoryScanned(strDirectory
);
548 // now scan the subfolders
549 for (int i
= 0; i
< items
.Size(); ++i
)
551 CFileItemPtr pItem
= items
[i
];
555 // if we have a directory item (non-playlist) we then recurse into that folder
556 if (pItem
->m_bIsFolder
&& !pItem
->IsParentFolder() && !pItem
->IsPlayList())
558 std::string strPath
=pItem
->GetPath();
559 if (!DoScan(strPath
))
568 CInfoScanner::INFO_RET
CMusicInfoScanner::ScanTags(const CFileItemList
& items
,
569 CFileItemList
& scannedItems
)
571 std::vector
<std::string
> regexps
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps
;
573 for (int i
= 0; i
< items
.Size(); ++i
)
576 return INFO_CANCELLED
;
578 CFileItemPtr pItem
= items
[i
];
580 if (CUtil::ExcludeFileOrFolder(pItem
->GetPath(), regexps
))
583 if (pItem
->m_bIsFolder
|| pItem
->IsPlayList() || pItem
->IsPicture() || MUSIC::IsLyrics(*pItem
))
588 CMusicInfoTag
& tag
= *pItem
->GetMusicInfoTag();
591 std::unique_ptr
<IMusicInfoTagLoader
> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem
));
592 if (nullptr != pLoader
)
593 pLoader
->Load(pItem
->GetPath(), tag
);
596 if (m_handle
&& m_itemCount
>0)
597 m_handle
->SetPercentage(static_cast<float>(m_currentItem
* 100) / static_cast<float>(m_itemCount
));
599 if (!tag
.Loaded() && !pItem
->HasCueDocument())
601 CLog::Log(LOGDEBUG
, "{} - No tag found for: {}", __FUNCTION__
, pItem
->GetPath());
606 if (!tag
.GetCueSheet().empty())
607 pItem
->LoadEmbeddedCue();
610 if (pItem
->HasCueDocument())
611 pItem
->LoadTracksFromCueDocument(scannedItems
);
613 scannedItems
.Add(pItem
);
618 static bool SortSongsByTrack(const CSong
& song
, const CSong
& song2
)
620 return song
.iTrack
< song2
.iTrack
;
623 void CMusicInfoScanner::FileItemsToAlbums(CFileItemList
& items
, VECALBUMS
& albums
, MAPSONGS
* songsMap
/* = NULL */)
626 * Step 1: Convert the FileItems into Songs.
627 * If they're MB tagged, create albums directly from the FileItems.
628 * If they're non-MB tagged, index them by album name ready for step 2.
630 std::map
<std::string
, VECSONGS
> songsByAlbumNames
;
631 for (int i
= 0; i
< items
.Size(); ++i
)
633 CMusicInfoTag
& tag
= *items
[i
]->GetMusicInfoTag();
634 CSong
song(*items
[i
]);
636 // keep the db-only fields intact on rescan...
637 if (songsMap
!= NULL
)
639 // Match up item to songs in library previously scanned with this path
640 MAPSONGS::iterator songlist
= songsMap
->find(items
[i
]->GetPath());
641 if (songlist
!= songsMap
->end())
643 VECSONGS::iterator foundsong
;
644 if (songlist
->second
.size() == 1)
645 foundsong
= songlist
->second
.begin();
648 // When filename mapped to multiple songs it is from cuesheet, match on disc/track number
649 int disctrack
= tag
.GetTrackAndDiscNumber();
650 foundsong
= std::find_if(songlist
->second
.begin(), songlist
->second
.end(),
651 [&](const CSong
& song
) { return disctrack
== song
.iTrack
; });
653 if (foundsong
!= songlist
->second
.end())
655 song
.idSong
= foundsong
->idSong
; // Reuse ID
656 song
.dateNew
= foundsong
->dateNew
; // Keep date originally created
657 song
.iTimesPlayed
= foundsong
->iTimesPlayed
;
658 song
.lastPlayed
= foundsong
->lastPlayed
;
659 if (song
.rating
== 0)
660 song
.rating
= foundsong
->rating
;
661 if (song
.userrating
== 0)
662 song
.userrating
= foundsong
->userrating
;
663 if (song
.strThumb
.empty())
664 song
.strThumb
= foundsong
->strThumb
;
669 if (!tag
.GetMusicBrainzAlbumID().empty())
671 VECALBUMS::iterator it
;
672 for (it
= albums
.begin(); it
!= albums
.end(); ++it
)
673 if (it
->strMusicBrainzAlbumID
== tag
.GetMusicBrainzAlbumID())
676 if (it
== albums
.end())
678 CAlbum
album(*items
[i
]);
679 album
.songs
.push_back(song
);
680 albums
.push_back(album
);
683 it
->songs
.push_back(song
);
686 songsByAlbumNames
[tag
.GetAlbum()].push_back(song
);
690 Step 2: Split into unique albums based on album name and album artist
691 In the case where the album artist is unknown, we use the primary artist
692 (i.e. first artist from each song).
694 for (auto& songsByAlbumName
: songsByAlbumNames
)
696 VECSONGS
& songs
= songsByAlbumName
.second
;
697 // sort the songs by tracknumber to identify duplicate track numbers
698 sort(songs
.begin(), songs
.end(), SortSongsByTrack
);
700 // map the songs to their primary artists
701 bool tracksOverlap
= false;
702 bool hasAlbumArtist
= false;
703 bool isCompilation
= true;
704 std::string old_DiscSubtitle
;
706 std::map
<std::string
, std::vector
<CSong
*> > artists
;
707 for (VECSONGS::iterator song
= songs
.begin(); song
!= songs
.end(); ++song
)
709 // test for song overlap
710 if (song
!= songs
.begin() && song
->iTrack
== (song
- 1)->iTrack
)
711 tracksOverlap
= true;
713 if (!song
->bCompilation
)
714 isCompilation
= false;
716 if (song
->strDiscSubtitle
!= old_DiscSubtitle
)
717 old_DiscSubtitle
= song
->strDiscSubtitle
;
719 // get primary artist
721 if (!song
->GetAlbumArtist().empty())
723 primary
= song
->GetAlbumArtist()[0];
724 hasAlbumArtist
= true;
726 else if (!song
->artistCredits
.empty())
727 primary
= song
->artistCredits
.begin()->GetArtist();
729 // add to the artist map
730 artists
[primary
].push_back(&(*song
));
734 We have a Various Artists compilation if
735 1. album name is non-empty AND
736 2a. no tracks overlap OR
737 2b. all tracks are marked as part of compilation AND
738 3a. a unique primary artist is specified as "various", "various artists" or the localized value
740 3b. we have at least two primary artists and no album artist specified.
742 std::string various
= g_localizeStrings
.Get(340); // Various Artists
744 !songsByAlbumName
.first
.empty() && (isCompilation
|| !tracksOverlap
); // 1+2b+2a
745 if (artists
.size() == 1)
747 std::string artist
= artists
.begin()->first
; StringUtils::ToLower(artist
);
748 if (!StringUtils::EqualsNoCase(artist
, "various") &&
749 !StringUtils::EqualsNoCase(artist
, "various artists") &&
750 !StringUtils::EqualsNoCase(artist
, various
)) // 3a
753 // Grab name for use in "various artist" artist
754 various
= artists
.begin()->first
;
756 else if (hasAlbumArtist
) // 3b
759 // Such a compilation album is stored under a unique artist that matches on Musicbrainz ID
760 // the "various artists" artist for music tagged with mbids.
763 CLog::Log(LOGDEBUG
, "Album '{}' is a compilation as there's no overlapping tracks and {}",
764 songsByAlbumName
.first
,
765 hasAlbumArtist
? "the album artist is 'Various'"
766 : "there is more than one unique artist");
767 // Clear song artists from artists map, put songs under "various artists" mbid entry
769 for (auto& song
: songs
)
770 artists
[VARIOUSARTISTS_MBID
].push_back(&song
);
774 We also have a compilation album when album name is non-empty and ALL tracks are marked as part of
775 a compilation even if an album artist is given, or all songs have the same primary artist. For
776 example an anthology - a collection of recordings from various old sources
777 combined together such as a "best of", retrospective or rarities type release.
779 Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b.
780 The album artist can be determined just like any normal album.
782 if (!compilation
&& !songsByAlbumName
.first
.empty() && isCompilation
)
786 "Album '{}' is a compilation as all songs are marked as part of a compilation",
787 songsByAlbumName
.first
);
791 Step 3: Find the common albumartist for each song and assign
792 albumartist to those tracks that don't have it set.
794 for (auto& j
: artists
)
796 /* Find the common artist(s) for these songs (grouped under primary artist).
797 Various artist compilations already under the unique "various artists" mbid.
798 Take from albumartist tag when present, or use artist tag.
799 When from albumartist tag also check albumartistsort tag and take first non-empty value
801 std::vector
<CSong
*>& artistSongs
= j
.second
;
802 std::vector
<std::string
> common
;
803 std::string albumartistsort
;
804 if (artistSongs
.front()->GetAlbumArtist().empty())
805 common
= artistSongs
.front()->GetArtist();
808 common
= artistSongs
.front()->GetAlbumArtist();
809 albumartistsort
= artistSongs
.front()->GetAlbumArtistSort();
811 for (std::vector
<CSong
*>::iterator k
= artistSongs
.begin() + 1; k
!= artistSongs
.end(); ++k
)
813 unsigned int match
= 0;
814 std::vector
<std::string
> compare
;
815 if ((*k
)->GetAlbumArtist().empty())
816 compare
= (*k
)->GetArtist();
819 compare
= (*k
)->GetAlbumArtist();
820 if (albumartistsort
.empty())
821 albumartistsort
= (*k
)->GetAlbumArtistSort();
823 for (; match
< common
.size() && match
< compare
.size(); match
++)
825 if (compare
[match
] != common
[match
])
828 common
.erase(common
.begin() + match
, common
.end());
830 if (j
.first
== VARIOUSARTISTS_MBID
)
833 common
.emplace_back(VARIOUSARTISTS_MBID
);
837 Step 4: Assign the album artist for each song that doesn't have it set
838 and add to the album vector
841 album
.strAlbum
= songsByAlbumName
.first
;
843 //Split the albumartist sort string to try and get sort names for individual artists
844 std::vector
<std::string
> sortnames
= StringUtils::Split(albumartistsort
, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
);
845 if (sortnames
.size() != common
.size())
846 // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
847 sortnames
= StringUtils::SplitMulti(sortnames
, { ";", ":", "|", "#" });
849 for (size_t i
= 0; i
< common
.size(); i
++)
851 if (common
[i
] == VARIOUSARTISTS_MBID
)
852 /* Treat "various", "various artists" and the localized equivalent name as the same
853 album artist as the artist with Musicbrainz ID 89ad4ac3-39f7-470e-963a-56509c546377.
854 If adding this artist for the first time then the name will be set to either the primary
855 artist read from tags when 3a, or the localized value for "various artists" when not 3a.
856 This means that tag values are no longer translated into the current language.
858 album
.artistCredits
.emplace_back(various
, VARIOUSARTISTS_MBID
);
861 album
.artistCredits
.emplace_back(StringUtils::Trim(common
[i
]));
862 // Set artist sort name providing we have as many as we have artists,
863 // otherwise something is wrong with them so ignore rather than guess.
864 if (sortnames
.size() == common
.size())
865 album
.artistCredits
.back().SetSortName(StringUtils::Trim(sortnames
[i
]));
868 album
.bCompilation
= compilation
;
869 for (auto& k
: artistSongs
)
871 if (k
->GetAlbumArtist().empty())
872 k
->SetAlbumArtist(common
);
873 //! @todo in future we may wish to union up the genres, for now we assume they're the same
874 album
.genre
= k
->genre
;
875 album
.strArtistSort
= k
->GetAlbumArtistSort();
876 // in addition, we may want to use release date as discriminating between albums
877 album
.strReleaseDate
= k
->strReleaseDate
,
878 album
.strLabel
= k
->strRecordLabel
;
879 album
.strType
= k
->strAlbumType
;
880 album
.songs
.push_back(*k
);
882 albums
.push_back(album
);
887 CInfoScanner::INFO_RET
888 CMusicInfoScanner::UpdateAlbumInfo(CAlbum
& album
,
889 const ADDON::ScraperPtr
& scraper
,
890 bool bAllowSelection
,
891 CGUIDialogProgress
* pDialog
)
893 m_musicDatabase
.Open();
894 INFO_RET result
= UpdateDatabaseAlbumInfo(album
, scraper
, bAllowSelection
, pDialog
);
895 m_musicDatabase
.Close();
899 CInfoScanner::INFO_RET
900 CMusicInfoScanner::UpdateArtistInfo(CArtist
& artist
,
901 const ADDON::ScraperPtr
& scraper
,
902 bool bAllowSelection
,
903 CGUIDialogProgress
* pDialog
)
905 m_musicDatabase
.Open();
906 INFO_RET result
= UpdateDatabaseArtistInfo(artist
, scraper
, bAllowSelection
, pDialog
);
907 m_musicDatabase
.Close();
911 int CMusicInfoScanner::RetrieveMusicInfo(const std::string
& strDirectory
, CFileItemList
& items
)
915 // get all information for all files in current directory from database, and remove them
916 if (m_musicDatabase
.RemoveSongsFromPath(strDirectory
, songsMap
))
917 m_needsCleanup
= true;
919 CFileItemList scannedItems
;
920 if (ScanTags(items
, scannedItems
) == INFO_CANCELLED
|| scannedItems
.Size() == 0)
924 FileItemsToAlbums(scannedItems
, albums
, &songsMap
);
927 Set thumb for songs and, if only one album in folder, store the thumb for
928 the album (music db) and the folder path (in Textures db) too.
929 The album and path thumb is either set to the folder art, or failing that to
930 the art embedded in the first music file.
931 Song thumb is only set when it varies, otherwise it is cleared so that it will
932 fallback to the album art (that may be from the first file, or that of the
933 folder or set later by scraping from NFO files or remote sources). Clearing
934 saves caching repeats of the same image.
936 However even if all songs are from one album this may not be the album
937 folder. It could be just a subfolder containing some of the songs from a disc
938 set e.g. CD1, CD2 etc., or the album could spread across many folders. In
939 this case the album art gets reset every time a folder with songs from just
940 that album is processed, and needs to be corrected later once all the parts
941 of the album have been scanned.
943 FindArtForAlbums(albums
, items
.GetPath());
945 /* Strategy: Having scanned tags and made a list of albums, add them to the library. Only then try
946 to scrape additional album and artist information. Music is often tagged to a mixed standard
947 - some albums have mbid tags, some don't. Once all the music files have been added to the library,
948 the mbid for an artist will be known even if it was only tagged on one song. The artist is best
949 scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
950 That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
951 even when it was not in the tagging for that album.
953 Doing scraping, generally the slower activity, in the background after scanning has fully populated
954 the library also means that the user can use their library to select music to play sooner.
959 // Add all albums to the library, and hence any new song or album artists or other contributors
960 for (auto& album
: albums
)
965 // mark albums without a title as singles
966 if (album
.strAlbum
.empty())
967 album
.releaseType
= CAlbum::Single
;
969 album
.strPath
= strDirectory
;
970 m_musicDatabase
.AddAlbum(album
, m_idSourcePath
);
971 m_albumsAdded
.insert(album
.idAlbum
);
973 numAdded
+= static_cast<int>(album
.songs
.size());
978 void MUSIC_INFO::CMusicInfoScanner::ScrapeInfoAddedAlbums()
980 /* Strategy: Having scanned tags, make a list of albums and add them to the library, only then try
981 to scrape additional album and artist information. Music is often tagged to a mixed standard
982 - some albums have mbid tags, some don't. Once all the music files have been added to the library,
983 the mbid for an artist will be known even if it was only tagged on one song. The artist is best
984 scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
985 That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
986 even when it was not in the tagging for that album.
988 Doing scraping, generally the slower activity, in the background after scanning has fully populated
989 the library also means that the user can use their library to select music to play sooner.
992 /* Scrape additional album and artist data.
993 For albums and artists without mbids, matching on album-artist pair can
994 be used to identify artist with greater accuracy than artist name alone.
995 Artist mbid returned by album scraper is used if we do not already have it.
996 Hence scrape album then related artists.
998 ADDON::AddonPtr addon
;
1000 ADDON::ScraperPtr albumScraper
;
1001 ADDON::ScraperPtr artistScraper
;
1002 if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS
, addon
))
1003 albumScraper
= std::dynamic_pointer_cast
<ADDON::CScraper
>(addon
);
1005 if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS
,
1007 artistScraper
= std::dynamic_pointer_cast
<ADDON::CScraper
>(addon
);
1009 bool albumartistsonly
= !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS
);
1011 if (!albumScraper
|| !artistScraper
)
1015 std::set
<int> artists
;
1016 for (auto albumId
: m_albumsAdded
)
1021 // Scrape album data
1023 if (!m_musicDatabase
.HasAlbumBeenScraped(albumId
))
1027 m_handle
->SetText(album
.GetAlbumArtistString() + " - " + album
.strAlbum
);
1028 m_handle
->SetProgress(i
, static_cast<int>(m_albumsAdded
.size()));
1031 // Fetch any artist mbids for album artist(s) and song artists when scraping those too.
1032 m_musicDatabase
.GetAlbum(albumId
, album
, !albumartistsonly
);
1033 UpdateDatabaseAlbumInfo(album
, albumScraper
, false);
1035 // Scrape information for artists that have not been scraped before, avoiding repeating
1036 // unsuccessful attempts for every album and song.
1037 for (const auto &artistCredit
: album
.artistCredits
)
1042 if (!m_musicDatabase
.HasArtistBeenScraped(artistCredit
.GetArtistId()) &&
1043 artists
.find(artistCredit
.GetArtistId()) == artists
.end())
1045 artists
.insert(artistCredit
.GetArtistId()); // Artist scraping attempted
1047 m_musicDatabase
.GetArtist(artistCredit
.GetArtistId(), artist
);
1048 UpdateDatabaseArtistInfo(artist
, artistScraper
, false);
1051 // Only scrape song artists if they are being displayed in artists node by default
1052 if (!albumartistsonly
)
1054 for (auto &song
: album
.songs
)
1058 for (const auto &artistCredit
: song
.artistCredits
)
1063 CMusicArtistInfo musicArtistInfo
;
1064 if (!m_musicDatabase
.HasArtistBeenScraped(artistCredit
.GetArtistId()) &&
1065 artists
.find(artistCredit
.GetArtistId()) == artists
.end())
1067 artists
.insert(artistCredit
.GetArtistId()); // Artist scraping attempted
1069 m_musicDatabase
.GetArtist(artistCredit
.GetArtistId(), artist
);
1070 UpdateDatabaseArtistInfo(artist
, artistScraper
, false);
1080 Set thumb for songs and the album(if only one album in folder).
1081 The album thumb is either set to the folder art, or failing that to the art
1082 embedded in the first music file. However this does not allow for there being
1083 other folders with more songs from the album e.g. this was a subfolder CD1
1084 and there is CD2 etc. yet to be processed
1085 Song thumb is only set when it varies, otherwise it is cleared so that it will
1086 fallback to the album art(that may be from the first file, or that of the
1087 folder or set later by scraping from NFO files or remote sources).Clearing
1088 saves caching repeats of the same image.
1090 void CMusicInfoScanner::FindArtForAlbums(VECALBUMS
&albums
, const std::string
&path
)
1093 If there's a single album in the folder, then art can be taken from
1096 std::string albumArt
;
1097 if (albums
.size() == 1)
1099 CFileItem
album(path
, true);
1101 If we are scanning a directory served over http(s) the root directory for an album will set
1102 IsInternetStream to true which prevents scanning it for art. As we can't reach this point
1103 without having read some tags (and tags are not read from streams) we can safely check for
1104 that case and set the IsHTTPDirectory property to enable scanning for art.
1106 if (StringUtils::StartsWithNoCase(path
, "http") && StringUtils::EndsWith(path
, "/"))
1107 album
.SetProperty("IsHTTPDirectory", true);
1108 albumArt
= album
.GetUserMusicThumb(true);
1109 if (!albumArt
.empty())
1110 albums
[0].art
["thumb"] = albumArt
;
1112 for (auto& album
: albums
)
1114 if (albums
.size() != 1)
1118 Find art that is common across these items
1119 If we find a single art image we treat it as the album art
1120 and discard song art else we use first as album art and
1121 keep everything as song art.
1123 bool singleArt
= true;
1125 for (auto& song
: album
.songs
)
1129 if (art
&& !art
->ArtMatches(song
))
1140 assign the first art found to the album - better than no art at all
1143 if (art
&& albumArt
.empty())
1145 if (!art
->strThumb
.empty())
1146 albumArt
= art
->strThumb
;
1148 albumArt
= CTextureUtils::GetWrappedImageURL(art
->strFileName
, "music");
1151 if (!albumArt
.empty())
1152 album
.art
["thumb"] = albumArt
;
1155 { //if singleArt then we can clear the artwork for all songs
1156 for (auto& k
: album
.songs
)
1160 { // more than one piece of art was found for these songs, so cache per song
1161 for (auto& k
: album
.songs
)
1163 if (k
.strThumb
.empty() && !k
.embeddedArt
.Empty())
1164 k
.strThumb
= CTextureUtils::GetWrappedImageURL(k
.strFileName
, "music");
1168 if (albums
.size() == 1 && !albumArt
.empty())
1170 // assign to folder thumb as well
1171 CFileItem
albumItem(path
, true);
1172 CMusicThumbLoader loader
;
1173 loader
.SetCachedImage(albumItem
, "thumb", albumArt
);
1177 void MUSIC_INFO::CMusicInfoScanner::RetrieveLocalArt()
1181 m_handle
->SetTitle(g_localizeStrings
.Get(506)); //"Checking media files..."
1182 //!@todo: title = Checking for local art
1185 std::set
<int> artistsArtDone
; // artists processed to avoid unsuccessful repeats
1187 for (auto albumId
: m_albumsAdded
)
1193 m_musicDatabase
.GetAlbum(albumId
, album
, false);
1196 m_handle
->SetText(album
.GetAlbumArtistString() + " - " + album
.strAlbum
);
1197 m_handle
->SetProgress(count
, static_cast<int>(m_albumsAdded
.size()));
1201 Automatically fetch local art from album folder and any disc sets subfolders
1203 Providing all songs from an album are are under a unique common album
1204 folder (no songs from other albums) then thumb has been set to local art,
1205 or failing that to embedded art, during scanning by FindArtForAlbums().
1206 But when songs are also spread over multiple subfolders within it e.g. disc
1207 sets, it will have been set to either the art of the last subfolder that was
1208 processed (if there is any), or from the first song in that subfolder with
1209 embedded art (if there is any). To correct this and find any thumb in the
1210 (common) album folder add "thumb" to those missing.
1212 AddAlbumArtwork(album
);
1215 Local album artist art
1217 Look in the nominated "Artist Information Folder" for thumbs and fanart.
1218 Failing that, for backward compatibility, fallback to the folder immediately
1219 above the album folder.
1220 It can only fallback if the album has a unique folder, and can only do so
1221 for the first album artist if the album is a collaboration e.g. composer,
1222 conductor, orchestra, or by several pop artists in their own right.
1223 Avoids repeatedly processing the same artist by maintaining a set.
1225 Adding the album may have added new artists, or provide art for an existing
1226 (song) artist, but does not replace any artwork already set. Hence once art
1227 has been found for an album artist, art is not searched for in other folders.
1229 It will find art for "various artists", if artwork is located above the
1230 folder containing compilatons.
1232 for (auto artistCredit
= album
.artistCredits
.begin(); artistCredit
!= album
.artistCredits
.end(); ++artistCredit
)
1236 int idArtist
= artistCredit
->GetArtistId();
1237 if (artistsArtDone
.find(idArtist
) == artistsArtDone
.end())
1239 artistsArtDone
.insert(idArtist
); // Artist processed
1241 // Get artist and subfolder within the Artist Information Folder
1243 m_musicDatabase
.GetArtist(idArtist
, artist
);
1244 m_musicDatabase
.GetArtistPath(artist
, artist
.strPath
);
1245 // Location of local art
1246 std::string artfolder
;
1247 if (CDirectory::Exists(artist
.strPath
))
1248 // When subfolder exists that is only place we look for local art
1249 artfolder
= artist
.strPath
;
1250 else if (!album
.strPath
.empty() && artistCredit
== album
.artistCredits
.begin())
1252 // If no individual artist subfolder has been found, for primary
1253 // album artist only look in the folder immediately above the album
1254 // folder. Not using GetOldArtistPath here because may not have not
1255 // have scanned all the albums yet.
1256 artfolder
= URIUtils::GetParentPath(album
.strPath
);
1258 AddArtistArtwork(artist
, artfolder
);
1264 int CMusicInfoScanner::GetPathHash(const CFileItemList
&items
, std::string
&hash
)
1266 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1267 if (0 == items
.Size()) return 0;
1268 CDigest digest
{CDigest::Type::MD5
};
1270 for (int i
= 0; i
< items
.Size(); ++i
)
1272 const CFileItemPtr pItem
= items
[i
];
1273 digest
.Update(pItem
->GetPath());
1274 digest
.Update((unsigned char *)&pItem
->m_dwSize
, sizeof(pItem
->m_dwSize
));
1275 KODI::TIME::FileTime time
= pItem
->m_dateTime
;
1276 digest
.Update((unsigned char*)&time
, sizeof(KODI::TIME::FileTime
));
1277 if (MUSIC::IsAudio(*pItem
) && !pItem
->IsPlayList() && !pItem
->IsNFO())
1280 hash
= digest
.Finalize();
1284 CInfoScanner::INFO_RET
1285 CMusicInfoScanner::UpdateDatabaseAlbumInfo(CAlbum
& album
,
1286 const ADDON::ScraperPtr
& scraper
,
1287 bool bAllowSelection
,
1288 CGUIDialogProgress
* pDialog
/* = NULL */)
1293 CMusicAlbumInfo albumInfo
;
1294 INFO_RET
albumDownloadStatus(INFO_CANCELLED
);
1295 std::string
origArtist(album
.GetAlbumArtistString());
1296 std::string
origAlbum(album
.strAlbum
);
1302 CLog::Log(LOGDEBUG
, "{} downloading info for: {}", __FUNCTION__
, album
.strAlbum
);
1303 albumDownloadStatus
= DownloadAlbumInfo(album
, scraper
, albumInfo
, !bAllowSelection
, pDialog
);
1304 if (albumDownloadStatus
== INFO_NOT_FOUND
)
1306 if (pDialog
&& bAllowSelection
)
1308 std::string
strTempAlbum(album
.strAlbum
);
1309 if (!CGUIKeyboardFactory::ShowAndGetInput(strTempAlbum
, CVariant
{ g_localizeStrings
.Get(16011) }, false))
1310 albumDownloadStatus
= INFO_CANCELLED
;
1313 std::string
strTempArtist(album
.GetAlbumArtistString());
1314 if (!CGUIKeyboardFactory::ShowAndGetInput(strTempArtist
, CVariant
{ g_localizeStrings
.Get(16025) }, false))
1315 albumDownloadStatus
= INFO_CANCELLED
;
1318 album
.strAlbum
= strTempAlbum
;
1319 album
.strArtistDesc
= strTempArtist
;
1326 auto eventLog
= CServiceBroker::GetEventLog();
1328 eventLog
->Add(EventPtr(new CMediaLibraryEvent(
1329 MediaTypeAlbum
, album
.strPath
, 24146,
1330 StringUtils::Format(g_localizeStrings
.Get(24147), MediaTypeAlbum
, album
.strAlbum
),
1331 CScraperUrl::GetThumbUrl(album
.thumbURL
.GetFirstUrlByType()),
1332 CURL::GetRedacted(album
.strPath
), EventLevel::Warning
)));
1337 // Restore original album and artist name, possibly changed by manual entry
1338 // to get info but should not change outside merge
1339 album
.strAlbum
= origAlbum
;
1340 album
.strArtistDesc
= origArtist
;
1342 if (albumDownloadStatus
== INFO_ADDED
)
1344 bool overridetags
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS
);
1345 // Remove art accidentally set by the Python scraper, it only provides URLs of possible artwork
1346 // Art is selected later applying whitelist and other art preferences
1347 albumInfo
.GetAlbum().art
.clear();
1348 album
.MergeScrapedAlbum(albumInfo
.GetAlbum(), overridetags
);
1349 m_musicDatabase
.UpdateAlbum(album
);
1350 albumInfo
.SetLoaded(true);
1354 // Fill any gaps with local art files or use first available from scraped URL list (when it has
1355 // been successfully scraped) as controlled by whitelist. Do this even when no info added
1356 // (cancelled, not found or error), there may be new local art files.
1357 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1358 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
) !=
1359 CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE
&&
1360 AddAlbumArtwork(album
))
1361 albumDownloadStatus
= INFO_ADDED
; // Local art added
1363 return albumDownloadStatus
;
1366 CInfoScanner::INFO_RET
1367 CMusicInfoScanner::UpdateDatabaseArtistInfo(CArtist
& artist
,
1368 const ADDON::ScraperPtr
& scraper
,
1369 bool bAllowSelection
,
1370 CGUIDialogProgress
* pDialog
/* = NULL */)
1375 CMusicArtistInfo artistInfo
;
1376 INFO_RET
artistDownloadStatus(INFO_CANCELLED
);
1377 std::string
origArtist(artist
.strArtist
);
1383 CLog::Log(LOGDEBUG
, "{} downloading info for: {}", __FUNCTION__
, artist
.strArtist
);
1384 artistDownloadStatus
= DownloadArtistInfo(artist
, scraper
, artistInfo
, !bAllowSelection
, pDialog
);
1385 if (artistDownloadStatus
== INFO_NOT_FOUND
)
1387 if (pDialog
&& bAllowSelection
)
1389 if (!CGUIKeyboardFactory::ShowAndGetInput(artist
.strArtist
, CVariant
{ g_localizeStrings
.Get(16025) }, false))
1390 artistDownloadStatus
= INFO_CANCELLED
;
1396 auto eventLog
= CServiceBroker::GetEventLog();
1398 eventLog
->Add(EventPtr(new CMediaLibraryEvent(
1399 MediaTypeArtist
, artist
.strPath
, 24146,
1400 StringUtils::Format(g_localizeStrings
.Get(24147), MediaTypeArtist
, artist
.strArtist
),
1401 CScraperUrl::GetThumbUrl(artist
.thumbURL
.GetFirstUrlByType()),
1402 CURL::GetRedacted(artist
.strPath
), EventLevel::Warning
)));
1407 // Restore original artist name, possibly changed by manual entry to get info
1408 // but should not change outside merge
1409 artist
.strArtist
= origArtist
;
1411 if (artistDownloadStatus
== INFO_ADDED
)
1413 artist
.MergeScrapedArtist(artistInfo
.GetArtist(), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS
));
1414 m_musicDatabase
.UpdateArtist(artist
);
1415 artistInfo
.SetLoaded();
1418 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1419 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
) ==
1420 CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE
)
1421 return artistDownloadStatus
;
1423 // Check artist art.
1424 // Fill any gaps with local art files, or use first available from scraped
1425 // list (when it has been successfully scraped). Do this even when no info
1426 // added (cancelled, not found or error), there may be new local art files.
1427 // Get individual artist subfolder within the Artist Information Folder
1428 m_musicDatabase
.GetArtistPath(artist
, artist
.strPath
);
1429 // Location of local art
1430 std::string artfolder
;
1431 if (CDirectory::Exists(artist
.strPath
))
1432 // When subfolder exists that is only place we look for art
1433 artfolder
= artist
.strPath
;
1436 // Fallback to the old location local to music files (when there is a
1437 // unique folder). If there is no folder for the artist, and *only* the
1438 // artist, this will be blank
1439 m_musicDatabase
.GetOldArtistPath(artist
.idArtist
, artfolder
);
1441 if (AddArtistArtwork(artist
, artfolder
))
1442 artistDownloadStatus
= INFO_ADDED
; // Local art added
1444 return artistDownloadStatus
; // Added, cancelled or not found
1447 #define THRESHOLD .95f
1449 CInfoScanner::INFO_RET
1450 CMusicInfoScanner::DownloadAlbumInfo(const CAlbum
& album
,
1451 const ADDON::ScraperPtr
& info
,
1452 CMusicAlbumInfo
& albumInfo
,
1453 bool bUseScrapedMBID
,
1454 CGUIDialogProgress
* pDialog
)
1458 m_handle
->SetTitle(StringUtils::Format(g_localizeStrings
.Get(20321), info
->Name()));
1459 m_handle
->SetText(album
.GetAlbumArtistString() + " - " + album
.strAlbum
);
1462 // clear our scraper cache
1465 CMusicInfoScraper
scraper(info
);
1466 bool bMusicBrainz
= false;
1468 When the mbid is derived from tags scraping of album information is done directly
1469 using that ID, otherwise the lookup is based on album and artist names and can mis-identify the
1470 album (i.e. classical music has many "Symphony No. 5"). To be able to correct any mistakes a
1471 manual refresh of artist information uses either the mbid if derived from tags or the album
1472 and artist names, not any previously scraped mbid.
1474 if (!album
.strMusicBrainzAlbumID
.empty() && (!album
.bScrapedMBID
|| bUseScrapedMBID
))
1476 CScraperUrl musicBrainzURL
;
1477 if (ResolveMusicBrainz(album
.strMusicBrainzAlbumID
, info
, musicBrainzURL
))
1479 CMusicAlbumInfo
albumNfo("nfo", musicBrainzURL
);
1480 scraper
.GetAlbums().clear();
1481 scraper
.GetAlbums().push_back(albumNfo
);
1482 bMusicBrainz
= true;
1487 bool existsNFO
= false;
1488 std::string path
= album
.strPath
;
1490 m_musicDatabase
.GetAlbumPath(album
.idAlbum
, path
);
1492 std::string strNfo
= URIUtils::AddFileToFolder(path
, "album.nfo");
1493 CInfoScanner::INFO_TYPE result
= CInfoScanner::NO_NFO
;
1495 existsNFO
= CFileUtils::Exists(strNfo
);
1496 // When on GUI ask user if they want to ignore nfo and refresh from Internet
1497 if (existsNFO
&& pDialog
&& CGUIDialogYesNo::ShowAndGetInput(10523, 20446))
1500 CLog::Log(LOGDEBUG
, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo
));
1504 CLog::Log(LOGDEBUG
, "Found matching nfo file: {}", CURL::GetRedacted(strNfo
));
1505 result
= nfoReader
.Create(strNfo
, info
);
1506 if (result
== CInfoScanner::FULL_NFO
)
1508 CLog::Log(LOGDEBUG
, "{} Got details from nfo", __FUNCTION__
);
1509 nfoReader
.GetDetails(albumInfo
.GetAlbum());
1512 else if (result
== CInfoScanner::URL_NFO
||
1513 result
== CInfoScanner::COMBINED_NFO
)
1515 CScraperUrl
scrUrl(nfoReader
.ScraperUrl());
1516 CMusicAlbumInfo
albumNfo("nfo",scrUrl
);
1517 ADDON::ScraperPtr nfoReaderScraper
= nfoReader
.GetScraperInfo();
1518 CLog::Log(LOGDEBUG
, "-- nfo-scraper: {}", nfoReaderScraper
->Name());
1519 CLog::Log(LOGDEBUG
, "-- nfo url: {}", scrUrl
.GetFirstThumbUrl());
1520 scraper
.SetScraperInfo(nfoReaderScraper
);
1521 scraper
.GetAlbums().clear();
1522 scraper
.GetAlbums().push_back(albumNfo
);
1524 else if (result
!= CInfoScanner::OVERRIDE_NFO
)
1525 CLog::Log(LOGERROR
, "Unable to find an url in nfo file: {}", strNfo
);
1528 if (!scraper
.CheckValidOrFallback(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER
)))
1529 { // the current scraper is invalid, as is the default - bail
1530 CLog::Log(LOGERROR
, "{} - current and default scrapers are invalid. Pick another one",
1535 if (!scraper
.GetAlbumCount())
1537 scraper
.FindAlbumInfo(album
.strAlbum
, album
.GetAlbumArtistString());
1539 while (!scraper
.Completed())
1544 return INFO_CANCELLED
;
1549 Finding album using xml scraper may request data from Musicbrainz.
1550 MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1551 returns 503 errors for all calls from that IP address.
1552 To stay below the rate-limit threshold wait 1s before proceeding
1554 if (!info
->IsPython())
1558 CGUIDialogSelect
*pDlg
= NULL
;
1559 int iSelectedAlbum
=0;
1560 if ((result
== CInfoScanner::NO_NFO
|| result
== CInfoScanner::OVERRIDE_NFO
)
1563 iSelectedAlbum
= -1; // set negative so that we can detect a failure
1564 if (scraper
.Succeeded() && scraper
.GetAlbumCount() >= 1)
1566 double bestRelevance
= 0;
1567 double minRelevance
= static_cast<double>(THRESHOLD
);
1568 if (pDialog
|| scraper
.GetAlbumCount() > 1) // score the matches
1570 //show dialog with all albums found
1573 pDlg
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogSelect
>(WINDOW_DIALOG_SELECT
);
1574 pDlg
->SetHeading(CVariant
{g_localizeStrings
.Get(181)});
1576 pDlg
->EnableButton(true, 413); // manual
1577 pDlg
->SetUseDetails(true);
1580 CFileItemList items
;
1581 for (int i
= 0; i
< scraper
.GetAlbumCount(); ++i
)
1583 CMusicAlbumInfo
& info
= scraper
.GetAlbum(i
);
1584 double relevance
= static_cast<double>(info
.GetRelevance());
1586 relevance
= CUtil::AlbumRelevance(info
.GetAlbum().strAlbum
, album
.strAlbum
,
1587 info
.GetAlbum().GetAlbumArtistString(),
1588 album
.GetAlbumArtistString());
1590 // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
1591 // otherwise, perfect matches only
1592 if (relevance
>= std::max(minRelevance
, bestRelevance
))
1593 { // we auto-select the best of these
1594 bestRelevance
= relevance
;
1599 // set the label to [relevance] album - artist
1600 std::string strTemp
= StringUtils::Format("[{:0.2f}] {}", relevance
, info
.GetTitle2());
1601 CFileItemPtr
item(new CFileItem("", false));
1602 item
->SetLabel(strTemp
);
1604 std::string strTemp2
;
1605 if (!scraper
.GetAlbum(i
).GetAlbum().strType
.empty())
1606 strTemp2
+= scraper
.GetAlbum(i
).GetAlbum().strType
;
1607 if (!scraper
.GetAlbum(i
).GetAlbum().strReleaseDate
.empty())
1608 strTemp2
+= " - " + scraper
.GetAlbum(i
).GetAlbum().strReleaseDate
;
1609 if (!scraper
.GetAlbum(i
).GetAlbum().strReleaseStatus
.empty())
1610 strTemp2
+= " - " + scraper
.GetAlbum(i
).GetAlbum().strReleaseStatus
;
1611 if (!scraper
.GetAlbum(i
).GetAlbum().strLabel
.empty())
1612 strTemp2
+= " - " + scraper
.GetAlbum(i
).GetAlbum().strLabel
;
1613 item
->SetLabel2(strTemp2
);
1615 item
->SetArt(scraper
.GetAlbum(i
).GetAlbum().art
);
1619 if (!pDialog
&& relevance
> 0.999) // we're so close, no reason to search further
1626 pDlg
->SetItems(items
);
1629 // and wait till user selects one
1630 if (pDlg
->GetSelectedItem() < 0)
1632 if (!pDlg
->IsButtonPressed())
1633 return INFO_CANCELLED
;
1635 // manual button pressed
1636 std::string strNewAlbum
= album
.strAlbum
;
1637 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum
, CVariant
{g_localizeStrings
.Get(16011)}, false))
1638 return INFO_CANCELLED
;
1639 if (strNewAlbum
== "")
1640 return INFO_CANCELLED
;
1642 std::string strNewArtist
= album
.GetAlbumArtistString();
1643 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist
, CVariant
{g_localizeStrings
.Get(16025)}, false))
1644 return INFO_CANCELLED
;
1646 pDialog
->SetLine(0, CVariant
{strNewAlbum
});
1647 pDialog
->SetLine(1, CVariant
{strNewArtist
});
1648 pDialog
->Progress();
1650 CAlbum newAlbum
= album
;
1651 newAlbum
.strAlbum
= strNewAlbum
;
1652 newAlbum
.strArtistDesc
= strNewArtist
;
1654 return DownloadAlbumInfo(newAlbum
, info
, albumInfo
, bUseScrapedMBID
, pDialog
);
1656 iSelectedAlbum
= pDlg
->GetSelectedItem();
1661 CMusicAlbumInfo
& info
= scraper
.GetAlbum(0);
1662 double relevance
= static_cast<double>(info
.GetRelevance());
1664 relevance
= CUtil::AlbumRelevance(info
.GetAlbum().strAlbum
,
1666 info
.GetAlbum().GetAlbumArtistString(),
1667 album
.GetAlbumArtistString());
1668 if (relevance
< static_cast<double>(THRESHOLD
))
1669 return INFO_NOT_FOUND
;
1675 if (iSelectedAlbum
< 0)
1676 return INFO_NOT_FOUND
;
1680 scraper
.LoadAlbumInfo(iSelectedAlbum
);
1681 while (!scraper
.Completed())
1686 return INFO_CANCELLED
;
1690 if (!scraper
.Succeeded())
1693 Fetching album details using xml scraper may makes requests for data from Musicbrainz.
1694 MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1695 returns 503 errors for all calls from that IP address.
1696 To stay below the rate-limit threshold wait 1s before proceeding incase next action is
1697 to scrape another album or artist
1699 if (!info
->IsPython())
1702 albumInfo
= scraper
.GetAlbum(iSelectedAlbum
);
1704 if (result
== CInfoScanner::COMBINED_NFO
|| result
== CInfoScanner::OVERRIDE_NFO
)
1705 nfoReader
.GetDetails(albumInfo
.GetAlbum(), NULL
, true);
1710 CInfoScanner::INFO_RET
1711 CMusicInfoScanner::DownloadArtistInfo(const CArtist
& artist
,
1712 const ADDON::ScraperPtr
& info
,
1713 MUSIC_GRABBER::CMusicArtistInfo
& artistInfo
,
1714 bool bUseScrapedMBID
,
1715 CGUIDialogProgress
* pDialog
)
1719 m_handle
->SetTitle(StringUtils::Format(g_localizeStrings
.Get(20320), info
->Name()));
1720 m_handle
->SetText(artist
.strArtist
);
1723 // clear our scraper cache
1726 CMusicInfoScraper
scraper(info
);
1727 bool bMusicBrainz
= false;
1729 When the mbid is derived from tags scraping of artist information is done directly
1730 using that ID, otherwise the lookup is based on name and can mis-identify the artist
1731 (many have same name). To be able to correct any mistakes a manual refresh of artist
1732 information uses either the mbid if derived from tags or the artist name, not any previously
1735 if (!artist
.strMusicBrainzArtistID
.empty() && (!artist
.bScrapedMBID
|| bUseScrapedMBID
))
1737 CScraperUrl musicBrainzURL
;
1738 if (ResolveMusicBrainz(artist
.strMusicBrainzArtistID
, info
, musicBrainzURL
))
1740 CMusicArtistInfo
artistNfo("nfo", musicBrainzURL
);
1741 scraper
.GetArtists().clear();
1742 scraper
.GetArtists().push_back(artistNfo
);
1743 bMusicBrainz
= true;
1748 CInfoScanner::INFO_TYPE result
= CInfoScanner::NO_NFO
;
1752 bool existsNFO
= false;
1753 // First look for nfo in the artists folder, the primary location
1754 path
= artist
.strPath
;
1755 // Get path when don't already have it.
1756 bool artistpathfound
= !path
.empty();
1757 if (!artistpathfound
)
1758 artistpathfound
= m_musicDatabase
.GetArtistPath(artist
, path
);
1759 if (artistpathfound
)
1761 strNfo
= URIUtils::AddFileToFolder(path
, "artist.nfo");
1762 existsNFO
= CFileUtils::Exists(strNfo
);
1765 // If not there fall back local to music files (historic location for those album artists with a unique folder)
1768 artistpathfound
= m_musicDatabase
.GetOldArtistPath(artist
.idArtist
, path
);
1769 if (artistpathfound
)
1771 strNfo
= URIUtils::AddFileToFolder(path
, "artist.nfo");
1772 existsNFO
= CFileUtils::Exists(strNfo
);
1775 CLog::Log(LOGDEBUG
, "{} not have path, nfo file not possible", artist
.strArtist
);
1778 // When on GUI ask user if they want to ignore nfo and refresh from Internet
1779 if (existsNFO
&& pDialog
&& CGUIDialogYesNo::ShowAndGetInput(21891, 20446))
1782 CLog::Log(LOGDEBUG
, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo
));
1787 CLog::Log(LOGDEBUG
, "Found matching nfo file: {}", CURL::GetRedacted(strNfo
));
1788 result
= nfoReader
.Create(strNfo
, info
);
1789 if (result
== CInfoScanner::FULL_NFO
)
1791 CLog::Log(LOGDEBUG
, "{} Got details from nfo", __FUNCTION__
);
1792 nfoReader
.GetDetails(artistInfo
.GetArtist());
1795 else if (result
== CInfoScanner::URL_NFO
|| result
== CInfoScanner::COMBINED_NFO
)
1797 CScraperUrl
scrUrl(nfoReader
.ScraperUrl());
1798 CMusicArtistInfo
artistNfo("nfo", scrUrl
);
1799 ADDON::ScraperPtr nfoReaderScraper
= nfoReader
.GetScraperInfo();
1800 CLog::Log(LOGDEBUG
, "-- nfo-scraper: {}", nfoReaderScraper
->Name());
1801 CLog::Log(LOGDEBUG
, "-- nfo url: {}", scrUrl
.GetFirstThumbUrl());
1802 scraper
.SetScraperInfo(nfoReaderScraper
);
1803 scraper
.GetArtists().push_back(artistNfo
);
1806 CLog::Log(LOGERROR
, "Unable to find an url in nfo file: {}", strNfo
);
1809 if (!scraper
.GetArtistCount())
1811 scraper
.FindArtistInfo(artist
.strArtist
);
1813 while (!scraper
.Completed())
1818 return INFO_CANCELLED
;
1823 Finding artist using xml scraper makes a request for data from Musicbrainz.
1824 MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter
1825 the server returns 503 errors for all calls from that IP address. To stay
1826 below the rate-limit threshold wait 1s before proceeding
1828 if (!info
->IsPython())
1832 int iSelectedArtist
= 0;
1833 if (result
== CInfoScanner::NO_NFO
&& !bMusicBrainz
)
1835 if (scraper
.GetArtistCount() >= 1)
1837 // now load the first match
1838 if (pDialog
&& scraper
.GetArtistCount() > 1)
1840 // if we found more then 1 album, let user choose one
1841 CGUIDialogSelect
*pDlg
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogSelect
>(WINDOW_DIALOG_SELECT
);
1844 pDlg
->SetHeading(CVariant
{g_localizeStrings
.Get(21890)});
1846 pDlg
->EnableButton(true, 413); // manual
1848 for (int i
= 0; i
< scraper
.GetArtistCount(); ++i
)
1850 // set the label to artist
1851 CFileItem
item(scraper
.GetArtist(i
).GetArtist());
1852 std::string strTemp
= scraper
.GetArtist(i
).GetArtist().strArtist
;
1853 if (!scraper
.GetArtist(i
).GetArtist().strBorn
.empty())
1854 strTemp
+= " ("+scraper
.GetArtist(i
).GetArtist().strBorn
+")";
1855 if (!scraper
.GetArtist(i
).GetArtist().strDisambiguation
.empty())
1856 strTemp
+= " - " + scraper
.GetArtist(i
).GetArtist().strDisambiguation
;
1857 if (!scraper
.GetArtist(i
).GetArtist().genre
.empty())
1859 std::string genres
= StringUtils::Join(scraper
.GetArtist(i
).GetArtist().genre
, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
);
1860 if (!genres
.empty())
1861 strTemp
= StringUtils::Format("[{}] {}", genres
, strTemp
);
1863 item
.SetLabel(strTemp
);
1864 item
.m_idepth
= i
; // use this to hold the index of the album in the scraper
1869 // and wait till user selects one
1870 if (pDlg
->GetSelectedItem() < 0)
1872 if (!pDlg
->IsButtonPressed())
1873 return INFO_CANCELLED
;
1875 // manual button pressed
1876 std::string strNewArtist
= artist
.strArtist
;
1877 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist
, CVariant
{g_localizeStrings
.Get(16025)}, false))
1878 return INFO_CANCELLED
;
1882 pDialog
->SetLine(0, CVariant
{strNewArtist
});
1883 pDialog
->Progress();
1887 newArtist
.strArtist
= strNewArtist
;
1888 return DownloadArtistInfo(newArtist
, info
, artistInfo
, bUseScrapedMBID
, pDialog
);
1890 iSelectedArtist
= pDlg
->GetSelectedFileItem()->m_idepth
;
1895 return INFO_NOT_FOUND
;
1898 Fetching artist details using xml scraper makes requests for data from Musicbrainz.
1899 MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
1900 returns 503 errors for all calls from that IP address.
1901 To stay below the rate-limit threshold wait 1s before proceeding incase next action is
1902 to scrape another album or artist
1904 if (!info
->IsPython())
1907 scraper
.LoadArtistInfo(iSelectedArtist
, artist
.strArtist
);
1908 while (!scraper
.Completed())
1913 return INFO_CANCELLED
;
1918 if (!scraper
.Succeeded())
1921 artistInfo
= scraper
.GetArtist(iSelectedArtist
);
1923 if (result
== CInfoScanner::COMBINED_NFO
)
1924 nfoReader
.GetDetails(artistInfo
.GetArtist(), NULL
, true);
1929 bool CMusicInfoScanner::ResolveMusicBrainz(const std::string
&strMusicBrainzID
, const ScraperPtr
&preferredScraper
, CScraperUrl
&musicBrainzURL
)
1931 // We have a MusicBrainz ID
1932 // Get a scraper that can resolve it to a MusicBrainz URL & force our
1933 // search directly to the specific album.
1934 bool bMusicBrainz
= false;
1937 musicBrainzURL
= preferredScraper
->ResolveIDToUrl(strMusicBrainzID
);
1939 catch (const ADDON::CScraperError
&sce
)
1945 if (musicBrainzURL
.HasUrls())
1947 CLog::Log(LOGDEBUG
, "-- nfo-scraper: {}", preferredScraper
->Name());
1948 CLog::Log(LOGDEBUG
, "-- nfo url: {}", musicBrainzURL
.GetFirstThumbUrl());
1949 bMusicBrainz
= true;
1952 return bMusicBrainz
;
1955 void CMusicInfoScanner::ScannerWait(unsigned int milliseconds
)
1957 if (milliseconds
> 10)
1960 m_StopEvent
.Wait(std::chrono::milliseconds(milliseconds
));
1963 std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds
));
1966 bool CMusicInfoScanner::AddArtistArtwork(CArtist
& artist
, const std::string
& artfolder
)
1968 if (!artist
.thumbURL
.HasUrls() && artfolder
.empty())
1969 return false; // No local or scraped possible art to process
1971 if (artist
.art
.empty())
1972 m_musicDatabase
.GetArtForItem(artist
.idArtist
, MediaTypeArtist
, artist
.art
);
1974 std::map
<std::string
, std::string
> addedart
;
1977 // Handle thumb separately, can be from multiple confgurable file names
1978 if (artist
.art
.find("thumb") == artist
.art
.end())
1980 if (!artfolder
.empty())
1981 { // Local music thumbnail images named by "musiclibrary.musicthumbs"
1982 CFileItem
item(artfolder
, true);
1983 strArt
= item
.GetUserMusicThumb(true);
1986 strArt
= CScraperUrl::GetThumbUrl(artist
.thumbURL
.GetFirstUrlByType("thumb"));
1987 if (!strArt
.empty())
1988 addedart
.insert(std::make_pair("thumb", strArt
));
1991 // Process additional art types in artist folder
1992 AddLocalArtwork(addedart
, MediaTypeArtist
, artist
.strArtist
, artfolder
);
1994 // Process remote artist art filling gaps with first of scraped art URLs
1995 AddRemoteArtwork(addedart
, MediaTypeArtist
, artist
.thumbURL
);
1997 int iArtLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1998 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
);
2000 for (const auto& it
: addedart
)
2002 // Cache thumb, fanart and other whitelisted artwork immediately
2003 // (other art types will be cached when first displayed)
2004 if (iArtLevel
!= CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL
|| it
.first
== "thumb" ||
2005 it
.first
== "fanart")
2006 CServiceBroker::GetTextureCache()->BackgroundCacheImage(it
.second
);
2007 auto ret
= artist
.art
.insert(it
);
2009 m_musicDatabase
.SetArtForItem(artist
.idArtist
, MediaTypeArtist
, it
.first
, it
.second
);
2011 return addedart
.size() > 0;
2014 bool CMusicInfoScanner::AddAlbumArtwork(CAlbum
& album
)
2016 // Fetch album path and any subfolders (disc sets).
2017 // No paths found when songs from different albums are in one folder
2018 std::vector
<std::pair
<std::string
, int>> paths
;
2019 m_musicDatabase
.GetAlbumPaths(album
.idAlbum
, paths
);
2020 for (const auto& pathpair
: paths
)
2022 if (album
.strPath
.empty())
2023 album
.strPath
= pathpair
.first
.c_str();
2025 // When more than one album path is the common path
2026 URIUtils::GetCommonPath(album
.strPath
, pathpair
.first
.c_str());
2029 if (!album
.thumbURL
.HasUrls() && album
.strPath
.empty())
2030 return false; // No local or scraped possible art to process
2032 if (album
.art
.empty())
2033 m_musicDatabase
.GetArtForItem(album
.idAlbum
, MediaTypeAlbum
, album
.art
);
2034 auto thumb
= album
.art
.find("thumb"); // Find "thumb", may want to replace it
2036 bool replaceThumb
= paths
.size() > 1;
2037 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2038 CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART
))
2040 // When "prefer online album art" enabled and we have a thumb as embedded art
2041 // then replace it if we find a scraped cover
2042 if (thumb
!= album
.art
.end() && StringUtils::StartsWith(thumb
->second
, "image://"))
2043 replaceThumb
= true;
2046 std::map
<std::string
, std::string
> addedart
;
2049 // Fetch local art from album folder
2050 // Handle thumbs separately, can be from multiple confgurable file names
2051 if (replaceThumb
|| thumb
== album
.art
.end())
2053 if (!album
.strPath
.empty())
2054 { // Local music thumbnail images named by "musiclibrary.musicthumbs"
2055 CFileItem
item(album
.strPath
, true);
2056 strArt
= item
.GetUserMusicThumb(true);
2059 strArt
= CScraperUrl::GetThumbUrl(album
.thumbURL
.GetFirstUrlByType("thumb"));
2060 if (!strArt
.empty())
2062 if (thumb
!= album
.art
.end())
2063 album
.art
.erase(thumb
);
2064 addedart
.insert(std::make_pair("thumb", strArt
));
2067 // Process additional art types in album folder
2068 AddLocalArtwork(addedart
, MediaTypeAlbum
, album
.strAlbum
, album
.strPath
);
2070 // Fetch local art from disc subfolders
2071 if (paths
.size() > 1)
2073 CMusicThumbLoader loader
;
2074 std::string firstDiscThumb
;
2075 int iDiscThumb
= 10000;
2076 for (const auto& pathpair
: paths
)
2080 int discnum
= m_musicDatabase
.GetDiscnumberForPathID(pathpair
.second
);
2083 // Handle thumbs separately. Get thumb for path from textures db cached during scan
2084 // (could be embedded or local file from multiple confgurable file names)
2085 CFileItem
item(pathpair
.first
.c_str(), true);
2086 std::string strArtType
= StringUtils::Format("{}{}", "thumb", discnum
);
2087 strArt
= loader
.GetCachedImage(item
, "thumb");
2089 strArt
= CScraperUrl::GetThumbUrl(album
.thumbURL
.GetFirstUrlByType(strArtType
));
2090 if (!strArt
.empty())
2092 addedart
.insert(std::make_pair(strArtType
, strArt
));
2093 // Store thumb of first disc with a thumb
2094 if (discnum
< iDiscThumb
)
2096 iDiscThumb
= discnum
;
2097 firstDiscThumb
= strArt
;
2101 // Process additional art types in disc subfolder
2102 AddLocalArtwork(addedart
, MediaTypeAlbum
, album
.strAlbum
, pathpair
.first
, discnum
);
2104 // Finally if we still don't have album thumb then use the art from the
2105 // first disc in the set with a thumb
2106 if (!firstDiscThumb
.empty() && album
.art
.find("thumb") == album
.art
.end())
2108 m_musicDatabase
.SetArtForItem(album
.idAlbum
, MediaTypeAlbum
, "thumb", firstDiscThumb
);
2109 // Assign art as folder thumb (in textures db) as well
2111 CFileItem
albumItem(album
.strPath
, true);
2112 loader
.SetCachedImage(albumItem
, "thumb", firstDiscThumb
);
2116 // Process remote album art filling gaps with first of scraped art URLs
2117 AddRemoteArtwork(addedart
, MediaTypeAlbum
, album
.thumbURL
);
2119 int iArtLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2120 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
);
2121 for (const auto& it
: addedart
)
2123 // Cache thumb, fanart and whitelisted artwork immediately
2124 // (other art types will be cached when first displayed)
2125 if (iArtLevel
!= CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL
|| it
.first
== "thumb" ||
2126 it
.first
== "fanart")
2127 CServiceBroker::GetTextureCache()->BackgroundCacheImage(it
.second
);
2129 auto ret
= album
.art
.insert(it
);
2131 m_musicDatabase
.SetArtForItem(album
.idAlbum
, MediaTypeAlbum
, it
.first
, it
.second
);
2133 return addedart
.size() > 0;
2136 std::vector
<CVariant
> CMusicInfoScanner::GetArtWhitelist(const MediaType
& mediaType
, int iArtLevel
)
2138 std::vector
<CVariant
> whitelistarttypes
;
2139 if (iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC
)
2141 // Basic artist artwork = thumb + fanart (but not "family" fanart1, fanart2 etc.)
2142 // Basic album artwork = thumb only, thumb handled separately not in whitelist
2143 if (mediaType
== MediaTypeArtist
)
2144 whitelistarttypes
.emplace_back("fanart");
2148 if (mediaType
== MediaTypeArtist
)
2149 whitelistarttypes
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2150 CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST
);
2152 whitelistarttypes
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2153 CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST
);
2156 return whitelistarttypes
;
2159 bool CMusicInfoScanner::AddLocalArtwork(std::map
<std::string
, std::string
>& art
,
2160 const std::string
& mediaType
,
2161 const std::string
& mediaName
,
2162 const std::string
& artfolder
,
2165 if (artfolder
.empty())
2168 int iArtLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2169 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
);
2171 std::vector
<CVariant
> whitelistarttypes
= GetArtWhitelist(mediaType
, iArtLevel
);
2172 bool bUseAll
= (iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL
) ||
2173 ((iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM
) &&
2174 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2175 CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART
));
2177 // Not useall and empty whitelist means no extra art is picked up from either place
2178 if (!bUseAll
&& whitelistarttypes
.empty())
2181 // Image files used as thumbs
2182 std::vector
<CVariant
> thumbs
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
2183 CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS
);
2186 CFileItemList availableArtFiles
;
2187 CDirectory::GetDirectory(artfolder
, availableArtFiles
,
2188 CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
2189 DIR_FLAG_NO_FILE_DIRS
| DIR_FLAG_READ_CACHE
| DIR_FLAG_NO_FILE_INFO
);
2191 for (const auto& artFile
: availableArtFiles
)
2193 if (artFile
->m_bIsFolder
)
2195 std::string strCandidate
= URIUtils::GetFileName(artFile
->GetPath());
2197 if (!mediaName
.empty() && StringUtils::StartsWith(strCandidate
, mediaName
))
2198 strCandidate
.erase(0, mediaName
.length());
2199 StringUtils::ToLower(strCandidate
);
2200 // Skip files already used as "thumb"
2201 // Typically folder.jpg but can be from multiple confgurable file names
2202 if (std::find(thumbs
.begin(), thumbs
.end(), strCandidate
) != thumbs
.end())
2204 // Grab and strip file extension
2206 size_t period
= strCandidate
.find_last_of("./\\");
2207 if (period
!= std::string::npos
&& strCandidate
[period
] == '.')
2209 strExt
= strCandidate
.substr(period
); // ".jpg", ".png" etc.
2210 strCandidate
.erase(period
); // "abc16" for file Abc16.jpg
2212 if (strCandidate
.empty())
2214 // Validate art type name
2215 size_t last_index
= strCandidate
.find_last_not_of("0123456789");
2216 std::string strDigits
= strCandidate
.substr(last_index
+ 1);
2217 std::string strFamily
= strCandidate
.substr(0, last_index
+ 1); // "abc" of "abc16"
2218 if (strFamily
.empty())
2220 if (!MUSIC_UTILS::IsValidArtType(strCandidate
))
2222 // Disc specific art from disc subfolder
2223 // Skip art where digits of filename do not match disc number
2224 if (discnum
> 0 && !strDigits
.empty() && (atoi(strDigits
.c_str()) != discnum
))
2227 // Use all art, or check for basic level art in whitelist exactly allowing for disc number,
2228 // or for custom art check whitelist contains art type family (strip trailing digits)
2229 // e.g. 'fanart', 'fanart1', 'fanart2' etc. all match whitelist entry 'fanart'
2230 std::string strCheck
= strCandidate
;
2231 if (discnum
> 0 || iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM
)
2232 strCheck
= strFamily
;
2233 if (bUseAll
|| std::find(whitelistarttypes
.begin(), whitelistarttypes
.end(), strCheck
) !=
2234 whitelistarttypes
.end())
2236 if (!strDigits
.empty())
2238 // Catch any variants of music thumbs e.g. folder2.jpg as "thumb2"
2239 // Used for disc sets when files all in one album folder
2240 if (std::find(thumbs
.begin(), thumbs
.end(), strFamily
+ strExt
) != thumbs
.end())
2241 strCandidate
= "thumb" + strDigits
;
2243 else if (discnum
> 0)
2244 // Append disc number when candidate art type (and file) not have it
2245 strCandidate
+= std::to_string(discnum
);
2247 if (art
.find(strCandidate
) == art
.end())
2248 art
.insert(std::make_pair(strCandidate
, artFile
->GetPath()));
2252 return art
.size() > 0;
2255 bool CMusicInfoScanner::AddRemoteArtwork(std::map
<std::string
, std::string
>& art
,
2256 const std::string
& mediaType
,
2257 const CScraperUrl
& thumbURL
)
2259 int iArtLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
2260 CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL
);
2262 std::vector
<CVariant
> whitelistarttypes
= GetArtWhitelist(mediaType
, iArtLevel
);
2263 bool bUseAll
= (iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL
) ||
2264 ((iArtLevel
== CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM
) &&
2265 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2266 CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART
));
2268 // not useall and empty whitelist means no extra art is picked up from either place
2269 if (!bUseAll
&& whitelistarttypes
.empty())
2273 // Done for artists and albums, so not need repeating at disc level
2274 for (const auto& url
: thumbURL
.GetUrls())
2276 // Art type is encoded into the scraper XML held in thumbURL as optional "aspect=" field.
2277 // Those URL without aspect are also returned for all other type values.
2278 // Loop through all the first URLS of each type except "thumb" and add if art missing
2279 if (url
.m_aspect
.empty() || url
.m_aspect
== "thumb")
2282 { // Check whitelist for art type family e.g. "discart" for aspect="discart2"
2283 std::string strName
= url
.m_aspect
;
2284 if (iArtLevel
!= CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC
)
2285 StringUtils::TrimRight(strName
, "0123456789");
2286 if (std::find(whitelistarttypes
.begin(), whitelistarttypes
.end(), strName
) ==
2287 whitelistarttypes
.end())
2290 if (art
.find(url
.m_aspect
) == art
.end())
2292 std::string strArt
= CScraperUrl::GetThumbUrl(url
);
2293 if (!strArt
.empty())
2294 art
.insert(std::make_pair(url
.m_aspect
, strArt
));
2298 return art
.size() > 0;
2301 // This function is the Run() function of the IRunnable
2302 // CFileCountReader and runs in a separate thread.
2303 void CMusicInfoScanner::Run()
2306 for (auto& it
: m_pathsToScan
)
2308 count
+= CountFilesRecursively(it
);
2310 m_itemCount
= count
;
2313 // Recurse through all folders we scan and count files
2314 int CMusicInfoScanner::CountFilesRecursively(const std::string
& strPath
)
2317 CFileItemList items
;
2318 CDirectory::GetDirectory(strPath
, items
, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), DIR_FLAG_NO_FILE_DIRS
);
2323 // true for recursive counting
2324 int count
= CountFiles(items
, true);
2328 int CMusicInfoScanner::CountFiles(const CFileItemList
&items
, bool recursive
)
2331 for (int i
=0; i
<items
.Size(); ++i
)
2333 const CFileItemPtr pItem
=items
[i
];
2335 if (recursive
&& pItem
->m_bIsFolder
)
2336 count
+=CountFilesRecursively(pItem
->GetPath());
2337 else if (MUSIC::IsAudio(*pItem
) && !pItem
->IsPlayList() && !pItem
->IsNFO())