2 * Copyright (C) 2005-2012 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "threads/SystemClock.h"
22 #include "MusicInfoScanner.h"
23 #include "music/tags/MusicInfoTagLoaderFactory.h"
24 #include "MusicAlbumInfo.h"
25 #include "MusicInfoScraper.h"
26 #include "filesystem/MusicDatabaseDirectory.h"
27 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
29 #include "utils/md5.h"
30 #include "GUIInfoManager.h"
31 #include "utils/Variant.h"
33 #include "music/tags/MusicInfoTag.h"
34 #include "guilib/GUIWindowManager.h"
35 #include "dialogs/GUIDialogExtendedProgressBar.h"
36 #include "dialogs/GUIDialogProgress.h"
37 #include "dialogs/GUIDialogSelect.h"
38 #include "guilib/GUIKeyboardFactory.h"
39 #include "filesystem/File.h"
40 #include "filesystem/Directory.h"
41 #include "settings/AdvancedSettings.h"
42 #include "settings/GUISettings.h"
43 #include "settings/Settings.h"
45 #include "guilib/LocalizeStrings.h"
46 #include "utils/StringUtils.h"
47 #include "utils/TimeUtils.h"
48 #include "utils/log.h"
49 #include "utils/URIUtils.h"
50 #include "TextureCache.h"
51 #include "music/MusicThumbLoader.h"
52 #include "interfaces/AnnouncementManager.h"
53 #include "GUIUserMessages.h"
58 using namespace MUSIC_INFO;
59 using namespace XFILE;
60 using namespace MUSIC_GRABBER;
62 CMusicInfoScanner::CMusicInfoScanner() : CThread("CMusicInfoScanner")
67 m_bCanInterrupt = false;
73 CMusicInfoScanner::~CMusicInfoScanner()
77 void CMusicInfoScanner::Process()
79 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::AudioLibrary, "xbmc", "OnScanStarted");
82 unsigned int tick = XbmcThreads::SystemClockMillis();
84 m_musicDatabase.Open();
86 if (m_showDialog && !g_guiSettings.GetBool("musiclibrary.backgroundupdate"))
88 CGUIDialogExtendedProgressBar* dialog =
89 (CGUIDialogExtendedProgressBar*)g_windowManager.GetWindow(WINDOW_DIALOG_EXT_PROGRESS);
90 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
93 m_bCanInterrupt = true;
95 if (m_scanType == 0) // load info from files
97 CLog::Log(LOGDEBUG, "%s - Starting scan", __FUNCTION__);
100 m_handle->SetTitle(g_localizeStrings.Get(505));
102 // Reset progress vars
106 // Create the thread to count all files to be scanned
107 SetPriority( GetMinPriority() );
108 CThread fileCountReader(this, "CMusicInfoScanner");
110 fileCountReader.Create();
112 // Database operations should not be canceled
113 // using Interupt() while scanning as it could
114 // result in unexpected behaviour.
115 m_bCanInterrupt = false;
116 m_needsCleanup = false;
119 bool cancelled = false;
120 while (!cancelled && m_pathsToScan.size())
123 * A copy of the directory path is used because the path supplied is
124 * immediately removed from the m_pathsToScan set in DoScan(). If the
125 * reference points to the entry in the set a null reference error
128 CStdString directory = *m_pathsToScan.begin();
129 if (!DoScan(directory))
136 g_infoManager.ResetLibraryBools();
142 m_handle->SetTitle(g_localizeStrings.Get(700));
143 m_handle->SetText("");
146 m_musicDatabase.CleanupOrphanedItems();
149 m_handle->SetTitle(g_localizeStrings.Get(331));
151 m_musicDatabase.Compress(false);
155 fileCountReader.StopThread();
157 m_musicDatabase.EmptyCache();
159 m_musicDatabase.Close();
160 CLog::Log(LOGDEBUG, "%s - Finished scan", __FUNCTION__);
162 tick = XbmcThreads::SystemClockMillis() - tick;
163 CLog::Log(LOGNOTICE, "My Music: Scanning for music info using worker thread, operation took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
166 if (m_scanType == 1) // load album info
168 int iCurrentItem = 1;
169 for (set<CAlbum>::iterator it=m_albumsToScan.begin();it != m_albumsToScan.end();++it)
173 m_handle->SetText(StringUtils::Join(it->artist, g_advancedSettings.m_musicItemSeparator)+" - "+it->strAlbum);
174 m_handle->SetPercentage(iCurrentItem++/(float)m_albumsToScan.size());
177 CMusicAlbumInfo albumInfo;
178 DownloadAlbumInfo(it->genre[0],StringUtils::Join(it->artist, g_advancedSettings.m_musicItemSeparator),it->strAlbum, bCanceled, albumInfo); // genre field holds path - see fetchalbuminfo()
180 if (m_bStop || bCanceled)
184 if (m_scanType == 2) // load artist info
187 for (set<CArtist>::iterator it=m_artistsToScan.begin();it != m_artistsToScan.end();++it)
191 m_handle->SetText(it->strArtist);
192 m_handle->SetPercentage(iCurrentItem++/(float)m_artistsToScan.size()*100);
195 DownloadArtistInfo(it->genre[0],it->strArtist,bCanceled); // genre field holds path - see fetchartistinfo()
197 if (m_bStop || bCanceled)
205 CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning.");
209 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::AudioLibrary, "xbmc", "OnScanFinished");
211 // we need to clear the musicdb cache and update any active lists
212 CUtil::DeleteMusicDatabaseDirectoryCache();
213 CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
214 g_windowManager.SendThreadMessage(msg);
217 m_handle->MarkFinished();
221 void CMusicInfoScanner::Start(const CStdString& strDirectory, int flags)
223 m_pathsToScan.clear();
224 m_albumsScanned.clear();
225 m_artistsScanned.clear();
228 if (strDirectory.IsEmpty())
229 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
231 m_musicDatabase.Open();
232 m_musicDatabase.GetPaths(m_pathsToScan);
233 m_musicDatabase.Close();
236 m_pathsToScan.insert(strDirectory);
237 m_pathsToCount = m_pathsToScan;
244 void CMusicInfoScanner::FetchAlbumInfo(const CStdString& strDirectory,
247 m_albumsToScan.clear();
248 m_albumsScanned.clear();
251 if (strDirectory.IsEmpty())
253 m_musicDatabase.Open();
254 m_musicDatabase.GetAlbumsNav("musicdb://3/", items);
255 m_musicDatabase.Close();
259 if (URIUtils::HasSlashAtEnd(strDirectory)) // directory
260 CDirectory::GetDirectory(strDirectory,items);
263 CFileItemPtr item(new CFileItem(strDirectory,false));
268 m_musicDatabase.Open();
269 for (int i=0;i<items.Size();++i)
271 if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
275 album.strAlbum = items[i]->GetMusicInfoTag()->GetAlbum();
276 album.artist = items[i]->GetMusicInfoTag()->GetArtist();
277 album.genre.push_back(items[i]->GetPath()); // a bit hacky use of field
278 m_albumsToScan.insert(album);
281 int id = m_musicDatabase.GetAlbumByName(album.strAlbum, album.artist);
283 m_musicDatabase.DeleteAlbumInfo(id);
286 m_musicDatabase.Close();
294 void CMusicInfoScanner::FetchArtistInfo(const CStdString& strDirectory,
297 m_artistsToScan.clear();
298 m_artistsScanned.clear();
301 if (strDirectory.IsEmpty())
303 m_musicDatabase.Open();
304 m_musicDatabase.GetArtistsNav("musicdb://2/", items, false, -1);
305 m_musicDatabase.Close();
309 if (URIUtils::HasSlashAtEnd(strDirectory)) // directory
310 CDirectory::GetDirectory(strDirectory,items);
313 CFileItemPtr newItem(new CFileItem(strDirectory,false));
318 m_musicDatabase.Open();
319 for (int i=0;i<items.Size();++i)
321 if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
325 artist.strArtist = StringUtils::Join(items[i]->GetMusicInfoTag()->GetArtist(), g_advancedSettings.m_musicItemSeparator);
326 artist.genre.push_back(items[i]->GetPath()); // a bit hacky use of field
327 m_artistsToScan.insert(artist);
330 int id = m_musicDatabase.GetArtistByName(artist.strArtist);
332 m_musicDatabase.DeleteArtistInfo(id);
335 m_musicDatabase.Close();
343 bool CMusicInfoScanner::IsScanning()
348 void CMusicInfoScanner::Stop()
351 m_musicDatabase.Interupt();
356 static void OnDirectoryScanned(const CStdString& strDirectory)
358 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
359 msg.SetStringParam(strDirectory);
360 g_windowManager.SendThreadMessage(msg);
363 static CStdString Prettify(const CStdString& strDirectory)
365 CURL url(strDirectory);
366 CStdString strStrippedPath = url.GetWithoutUserDetails();
367 CURL::Decode(strStrippedPath);
369 return strStrippedPath;
372 bool CMusicInfoScanner::DoScan(const CStdString& strDirectory)
375 m_handle->SetText(Prettify(strDirectory));
378 * remove this path from the list we're processing. This must be done prior to
379 * the check for file or folder exclusion to prevent an infinite while loop
382 set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
383 if (it != m_pathsToScan.end())
384 m_pathsToScan.erase(it);
386 // Discard all excluded files defined by m_musicExcludeRegExps
388 CStdStringArray regexps = g_advancedSettings.m_audioExcludeFromScanRegExps;
390 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
395 CDirectory::GetDirectory(strDirectory, items, g_settings.m_musicExtensions + "|.jpg|.tbn|.lrc|.cdg");
397 // sort and get the path hash. Note that we don't filter .cue sheet items here as we want
398 // to detect changes in the .cue sheet as well. The .cue sheet items only need filtering
399 // if we have a changed hash.
400 items.Sort(SORT_METHOD_LABEL, SortOrderAscending);
402 GetPathHash(items, hash);
404 // check whether we need to rescan or not
406 if ((m_flags & SCAN_RESCAN) || !m_musicDatabase.GetPathHash(strDirectory, dbHash) || dbHash != hash)
407 { // path has changed - rescan
408 if (dbHash.IsEmpty())
409 CLog::Log(LOGDEBUG, "%s Scanning dir '%s' as not in the database", __FUNCTION__, strDirectory.c_str());
411 CLog::Log(LOGDEBUG, "%s Rescanning dir '%s' due to change", __FUNCTION__, strDirectory.c_str());
413 // filter items in the sub dir (for .cue sheet support)
414 items.FilterCueItems();
415 items.Sort(SORT_METHOD_LABEL, SortOrderAscending);
417 // and then scan in the new information
418 if (RetrieveMusicInfo(items, strDirectory) > 0)
421 OnDirectoryScanned(strDirectory);
424 // save information about this folder
425 m_musicDatabase.SetPathHash(strDirectory, hash);
428 { // path is the same - no need to rescan
429 CLog::Log(LOGDEBUG, "%s Skipping dir '%s' due to no change", __FUNCTION__, strDirectory.c_str());
430 m_currentItem += CountFiles(items, false); // false for non-recursive
432 // updated the dialog with our progress
436 m_handle->SetPercentage(m_currentItem/(float)m_itemCount*100);
437 OnDirectoryScanned(strDirectory);
441 // now scan the subfolders
442 for (int i = 0; i < items.Size(); ++i)
444 CFileItemPtr pItem = items[i];
448 // if we have a directory item (non-playlist) we then recurse into that folder
449 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList())
451 CStdString strPath=pItem->GetPath();
452 if (!DoScan(strPath))
462 int CMusicInfoScanner::RetrieveMusicInfo(CFileItemList& items, const CStdString& strDirectory)
466 // get all information for all files in current directory from database, and remove them
467 if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap))
468 m_needsCleanup = true;
472 CStdStringArray regexps = g_advancedSettings.m_audioExcludeFromScanRegExps;
474 // for every file found, but skip folder
475 for (int i = 0; i < items.Size(); ++i)
477 CFileItemPtr pItem = items[i];
478 CStdString strExtension;
479 URIUtils::GetExtension(pItem->GetPath(), strExtension);
484 // Discard all excluded files defined by m_musicExcludeRegExps
485 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps))
488 // dont try reading id3tags for folders, playlists or shoutcast streams
489 if (!pItem->m_bIsFolder && !pItem->IsPlayList() && !pItem->IsPicture() && !pItem->IsLyrics() )
492 // CLog::Log(LOGDEBUG, "%s - Reading tag for: %s", __FUNCTION__, pItem->GetPath().c_str());
494 // grab info from the song
495 CSong *dbSong = songsMap.Find(pItem->GetPath());
497 CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
499 { // read the tag from a file
500 auto_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(pItem->GetPath()));
501 if (NULL != pLoader.get())
502 pLoader->Load(pItem->GetPath(), tag);
505 // if we have the itemcount, update our
506 // dialog with the progress we made
507 if (m_handle && m_itemCount>0)
508 m_handle->SetPercentage(m_currentItem/(float)m_itemCount*100);
514 // ensure our song has a valid filename or else it will assert in AddSong()
515 if (song.strFileName.IsEmpty())
517 // copy filename from path in case UPnP or other tag loaders didn't specify one (FIXME?)
518 song.strFileName = pItem->GetPath();
520 // if we still don't have a valid filename, skip the song
521 if (song.strFileName.IsEmpty())
523 // this shouldn't ideally happen!
524 CLog::Log(LOGERROR, "Skipping song since it doesn't seem to have a filename");
529 song.iStartOffset = pItem->m_lStartOffset;
530 song.iEndOffset = pItem->m_lEndOffset;
531 song.strThumb = pItem->GetUserMusicThumb(true);
533 { // keep the db-only fields intact on rescan...
534 song.iTimesPlayed = dbSong->iTimesPlayed;
535 song.lastPlayed = dbSong->lastPlayed;
536 song.iKaraokeNumber = dbSong->iKaraokeNumber;
538 if (song.rating == '0') song.rating = dbSong->rating;
539 if (song.strThumb.empty())
540 song.strThumb = dbSong->strThumb;
542 songsToAdd.push_back(song);
543 // CLog::Log(LOGDEBUG, "%s - Tag loaded for: %s", __FUNCTION__, pItem->GetPath().c_str());
546 CLog::Log(LOGDEBUG, "%s - No tag found for: %s", __FUNCTION__, pItem->GetPath().c_str());
551 CategoriseAlbums(songsToAdd, albums);
552 FindArtForAlbums(albums, items.GetPath());
554 // finally, add these to the database
555 m_musicDatabase.BeginTransaction();
557 set<int> albumsToScan;
558 set<int> artistsToScan;
559 for (VECALBUMS::iterator i = albums.begin(); i != albums.end(); ++i)
562 int idAlbum = m_musicDatabase.AddAlbum(*i, songIDs);
563 numAdded += i->songs.size();
566 m_musicDatabase.RollbackTransaction();
570 // Build the artist & album sets
571 albumsToScan.insert(idAlbum);
572 for (vector<int>::iterator j = songIDs.begin(); j != songIDs.end(); ++j)
574 vector<int> songArtists;
575 m_musicDatabase.GetArtistsBySong(*j, false, songArtists);
576 artistsToScan.insert(songArtists.begin(), songArtists.end());
578 std::vector<int> albumArtists;
579 m_musicDatabase.GetArtistsByAlbum(idAlbum, false, albumArtists);
580 artistsToScan.insert(albumArtists.begin(), albumArtists.end());
582 m_musicDatabase.CommitTransaction();
584 // Download info & artwork
586 for (set<int>::iterator it = artistsToScan.begin(); it != artistsToScan.end(); ++it)
589 if (find(m_artistsScanned.begin(),m_artistsScanned.end(), *it) == m_artistsScanned.end())
591 CStdString strArtist = m_musicDatabase.GetArtistById(*it);
592 m_artistsScanned.push_back(*it);
593 if (!m_bStop && (m_flags & SCAN_ONLINE))
596 strPath.Format("musicdb://2/%u/", *it);
598 if (!DownloadArtistInfo(strPath, strArtist, bCanceled)) // assume we want to retry
599 m_artistsScanned.pop_back();
603 map<string, string> artwork = GetArtistArtwork(*it);
604 m_musicDatabase.SetArtForItem(*it, "artist", artwork);
609 if (m_flags & SCAN_ONLINE)
611 for (set<int>::iterator it = albumsToScan.begin(); it != albumsToScan.end(); ++it)
614 return songsToAdd.size();
617 strPath.Format("musicdb://3/%u/",*it);
620 m_musicDatabase.GetAlbumInfo(*it, album, NULL);
622 if (find(m_albumsScanned.begin(), m_albumsScanned.end(), *it) == m_albumsScanned.end())
624 CMusicAlbumInfo albumInfo;
625 if (DownloadAlbumInfo(strPath, StringUtils::Join(album.artist, g_advancedSettings.m_musicItemSeparator), album.strAlbum, bCanceled, albumInfo))
626 m_albumsScanned.push_back(*it);
631 m_handle->SetTitle(g_localizeStrings.Get(505));
633 return songsToAdd.size();
636 static bool SortSongsByTrack(CSong *song, CSong *song2)
638 return song->iTrack < song2->iTrack;
641 void CMusicInfoScanner::CategoriseAlbums(VECSONGS &songsToCheck, VECALBUMS &albums)
643 /* Step 1: categorise on the album name */
644 map<string, vector<CSong *> > albumNames;
645 for (VECSONGS::iterator i = songsToCheck.begin(); i != songsToCheck.end(); ++i)
646 albumNames[i->strAlbum].push_back(&(*i));
649 Step 2: Split into unique albums based on album name and album artist
650 In the case where the album artist is unknown, we use the primary artist
651 (i.e. first artist from each song).
654 for (map<string, vector<CSong *> >::iterator i = albumNames.begin(); i != albumNames.end(); ++i)
656 // sort the songs by tracknumber to identify duplicate track numbers
657 vector<CSong *> &songs = i->second;
658 sort(songs.begin(), songs.end(), SortSongsByTrack);
660 // map the songs to their primary artists
661 bool tracksOverlap = false;
662 bool hasAlbumArtist = false;
664 map<string, vector<CSong *> > artists;
665 for (vector<CSong *>::iterator j = songs.begin(); j != songs.end(); ++j)
668 // test for song overlap
669 if (j != songs.begin() && song->iTrack == (*(j-1))->iTrack)
670 tracksOverlap = true;
672 // get primary artist
674 if (!song->albumArtist.empty())
676 primary = song->albumArtist[0];
677 hasAlbumArtist = true;
679 else if (!song->artist.empty())
680 primary = song->artist[0];
682 // add to the artist map
683 artists[primary].push_back(song);
687 We have a compilation if
688 1. album name is non-empty AND
689 2. no tracks overlap AND
690 3a. a unique primary artist is specified as "various" or "various artists" OR
691 3b. we have at least two primary artists and no album artist specified.
693 bool compilation = !i->first.empty() && !tracksOverlap; // 1+2
694 if (artists.size() == 1)
696 string artist = artists.begin()->first; StringUtils::ToLower(artist);
697 if (!StringUtils::EqualsNoCase(artist, "various") &&
698 !StringUtils::EqualsNoCase(artist, "various artists")) // 3a
701 else if (hasAlbumArtist) // 3b
706 CLog::Log(LOGDEBUG, "Album '%s' is a compilation as there's no overlapping tracks and %s", i->first.c_str(), hasAlbumArtist ? "the album artist is 'Various'" : "there is more than one unique artist");
708 std::string various = g_localizeStrings.Get(340); // Various Artists
709 vector<string> va; va.push_back(various);
710 for (vector<CSong *>::iterator j = songs.begin(); j != songs.end(); ++j)
711 (*j)->albumArtist = va;
712 artists.insert(make_pair(various, songs));
716 Step 3: Find the common albumartist for each song and assign
717 albumartist to those tracks that don't have it set.
719 for (map<string, vector<CSong *> >::iterator j = artists.begin(); j != artists.end(); ++j)
721 // find the common artist for these songs
722 vector<CSong *> &artistSongs = j->second;
723 vector<string> common = artistSongs.front()->albumArtist.empty() ? artistSongs.front()->artist : artistSongs.front()->albumArtist;
724 for (vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k)
726 unsigned int match = 0;
727 vector<string> &compare = (*k)->albumArtist.empty() ? (*k)->artist : (*k)->albumArtist;
728 for (; match < common.size() && match < compare.size(); match++)
730 if (compare[match] != common[match])
733 common.erase(common.begin() + match, common.end());
737 Step 4: Assign the album artist for each song that doesn't have it set
738 and add to the album vector
741 album.strAlbum = i->first;
742 album.artist = common;
743 album.bCompilation = compilation;
744 for (vector<CSong *>::iterator k = artistSongs.begin(); k != artistSongs.end(); ++k)
746 if ((*k)->albumArtist.empty())
747 (*k)->albumArtist = common;
748 album.songs.push_back(*(*k));
749 // TODO: in future we may wish to union up the genres, for now we assume they're the same
750 if (album.genre.empty())
751 album.genre = (*k)->genre;
752 // in addition, we may want to use year as discriminating for albums
753 if (album.iYear == 0)
754 album.iYear = (*k)->iYear;
757 albums.push_back(album);
762 void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const CStdString &path)
765 If there's a single album in the folder, then art can be taken from
768 std::string albumArt;
769 if (albums.size() == 1)
771 CFileItem album(path, true);
772 albumArt = album.GetUserMusicThumb(true);
773 if (!albumArt.empty())
774 albums[0].art["thumb"] = albumArt;
776 for (VECALBUMS::iterator i = albums.begin(); i != albums.end(); ++i)
780 if (albums.size() != 1)
784 Find art that is common across these items
785 If we find a single art image we treat it as the album art
786 and discard song art else we use first as album art and
787 keep everything as song art.
789 bool singleArt = true;
791 for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k)
796 if (art && !art->ArtMatches(song))
807 assign the first art found to the album - better than no art at all
810 if (art && albumArt.empty())
812 if (!art->strThumb.empty())
813 albumArt = art->strThumb;
815 albumArt = CTextureCache::GetWrappedImageURL(art->strFileName, "music");
818 if (!albumArt.empty())
819 album.art["thumb"] = albumArt;
822 { //if singleArt then we can clear the artwork for all songs
823 for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k)
827 { // more than one piece of art was found for these songs, so cache per song
828 for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k)
830 if (k->strThumb.empty() && !k->embeddedArt.empty())
831 k->strThumb = CTextureCache::GetWrappedImageURL(k->strFileName, "music");
835 if (albums.size() == 1 && !albumArt.empty())
836 { // assign to folder thumb as well
837 CMusicThumbLoader::SetCachedImage(path, "thumb", albumArt);
841 // This function is run by another thread
842 void CMusicInfoScanner::Run()
845 while (!m_bStop && m_pathsToCount.size())
846 count+=CountFilesRecursively(*m_pathsToCount.begin());
850 // Recurse through all folders we scan and count files
851 int CMusicInfoScanner::CountFilesRecursively(const CStdString& strPath)
855 // CLog::Log(LOGDEBUG, __FUNCTION__" - processing dir: %s", strPath.c_str());
856 CDirectory::GetDirectory(strPath, items, g_settings.m_musicExtensions, DIR_FLAG_NO_FILE_DIRS);
861 // true for recursive counting
862 int count = CountFiles(items, true);
864 // remove this path from the list we're processing
865 set<CStdString>::iterator it = m_pathsToCount.find(strPath);
866 if (it != m_pathsToCount.end())
867 m_pathsToCount.erase(it);
869 // CLog::Log(LOGDEBUG, __FUNCTION__" - finished processing dir: %s", strPath.c_str());
873 int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive)
876 for (int i=0; i<items.Size(); ++i)
878 const CFileItemPtr pItem=items[i];
880 if (recursive && pItem->m_bIsFolder)
881 count+=CountFilesRecursively(pItem->GetPath());
882 else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
888 int CMusicInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
890 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
891 if (0 == items.Size()) return 0;
892 XBMC::XBMC_MD5 md5state;
894 for (int i = 0; i < items.Size(); ++i)
896 const CFileItemPtr pItem = items[i];
897 md5state.append(pItem->GetPath());
898 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
899 FILETIME time = pItem->m_dateTime;
900 md5state.append((unsigned char *)&time, sizeof(FILETIME));
901 if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
904 md5state.getDigest(hash);
908 #define THRESHOLD .95f
910 bool CMusicInfoScanner::DownloadAlbumInfo(const CStdString& strPath, const CStdString& strArtist, const CStdString& strAlbum, bool& bCanceled, CMusicAlbumInfo& albumInfo, CGUIDialogProgress* pDialog)
914 XFILE::MUSICDATABASEDIRECTORY::CQueryParams params;
915 XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strPath, params);
917 m_musicDatabase.Open();
918 if (m_musicDatabase.HasAlbumInfo(params.GetAlbumId()) && m_musicDatabase.GetAlbumInfo(params.GetAlbumId(),album,&songs))
922 ADDON::ScraperPtr info;
923 if (!m_musicDatabase.GetScraperForPath(strPath, info, ADDON::ADDON_SCRAPER_ALBUMS) || !info)
925 m_musicDatabase.Close();
931 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20321), info->Name().c_str()));
932 m_handle->SetText(strArtist+" - "+strAlbum);
935 // clear our scraper cache
938 CMusicInfoScraper scraper(info);
941 CStdString strAlbumPath, strNfo;
942 m_musicDatabase.GetAlbumPath(params.GetAlbumId(),strAlbumPath);
943 URIUtils::AddFileToFolder(strAlbumPath,"album.nfo",strNfo);
944 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
946 if (XFILE::CFile::Exists(strNfo))
948 CLog::Log(LOGDEBUG,"Found matching nfo file: %s", strNfo.c_str());
949 result = nfoReader.Create(strNfo, info, -1, strPath);
950 if (result == CNfoFile::FULL_NFO)
952 CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
954 nfoReader.GetDetails(album);
955 m_musicDatabase.SetAlbumInfo(params.GetAlbumId(), album, album.songs);
956 GetAlbumArtwork(params.GetAlbumId(), album);
957 m_musicDatabase.Close();
960 else if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
962 CScraperUrl scrUrl(nfoReader.ScraperUrl());
963 CMusicAlbumInfo album("nfo",scrUrl);
964 info = nfoReader.GetScraperInfo();
965 CLog::Log(LOGDEBUG,"-- nfo-scraper: %s",info->Name().c_str());
966 CLog::Log(LOGDEBUG,"-- nfo url: %s", scrUrl.m_url[0].m_url.c_str());
967 scraper.SetScraperInfo(info);
968 scraper.GetAlbums().push_back(album);
971 CLog::Log(LOGERROR,"Unable to find an url in nfo file: %s", strNfo.c_str());
974 if (!scraper.CheckValidOrFallback(g_guiSettings.GetString("musiclibrary.albumsscraper")))
975 { // the current scraper is invalid, as is the default - bail
976 CLog::Log(LOGERROR, "%s - current and default scrapers are invalid. Pick another one", __FUNCTION__);
980 if (!scraper.GetAlbumCount())
982 scraper.FindAlbumInfo(strAlbum, strArtist);
984 while (!scraper.Completed())
995 CGUIDialogSelect *pDlg=NULL;
996 int iSelectedAlbum=0;
997 if (result == CNfoFile::NO_NFO)
999 iSelectedAlbum = -1; // set negative so that we can detect a failure
1000 if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1)
1003 double bestRelevance = 0;
1004 double minRelevance = THRESHOLD;
1005 if (scraper.GetAlbumCount() > 1) // score the matches
1007 //show dialog with all albums found
1010 pDlg = (CGUIDialogSelect*)g_windowManager.GetWindow(WINDOW_DIALOG_SELECT);
1011 pDlg->SetHeading(g_localizeStrings.Get(181).c_str());
1013 pDlg->EnableButton(true, 413); // manual
1016 for (int i = 0; i < scraper.GetAlbumCount(); ++i)
1018 CMusicAlbumInfo& info = scraper.GetAlbum(i);
1019 double relevance = info.GetRelevance();
1021 relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, strAlbum, StringUtils::Join(info.GetAlbum().artist, g_advancedSettings.m_musicItemSeparator), strArtist);
1023 // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
1024 // otherwise, perfect matches only
1025 if (relevance >= max(minRelevance, bestRelevance))
1026 { // we auto-select the best of these
1027 bestRelevance = relevance;
1032 // set the label to [relevance] album - artist
1034 strTemp.Format("[%0.2f] %s", relevance, info.GetTitle2());
1035 CFileItem item(strTemp);
1036 item.m_idepth = i; // use this to hold the index of the album in the scraper
1039 if (relevance > .99f) // we're so close, no reason to search further
1045 CMusicAlbumInfo& info = scraper.GetAlbum(0);
1046 double relevance = info.GetRelevance();
1048 relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, strAlbum, StringUtils::Join(info.GetAlbum().artist, g_advancedSettings.m_musicItemSeparator), strArtist);
1049 if (relevance < THRESHOLD)
1051 m_musicDatabase.Close();
1054 bestRelevance = relevance;
1058 iSelectedAlbum = bestMatch;
1059 if (pDialog && bestRelevance < THRESHOLD)
1064 // and wait till user selects one
1065 if (pDlg->GetSelectedLabel() < 0)
1067 if (!pDlg->IsButtonPressed())
1072 // manual button pressed
1073 CStdString strNewAlbum = strAlbum;
1074 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum, g_localizeStrings.Get(16011), false)) return false;
1075 if (strNewAlbum == "") return false;
1077 CStdString strNewArtist = strArtist;
1078 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, g_localizeStrings.Get(16025), false)) return false;
1080 pDialog->SetLine(0, strNewAlbum);
1081 pDialog->SetLine(1, strNewArtist);
1082 pDialog->Progress();
1084 m_musicDatabase.Close();
1085 return DownloadAlbumInfo(strPath,strNewArtist,strNewAlbum,bCanceled,albumInfo,pDialog);
1087 iSelectedAlbum = pDlg->GetSelectedItem()->m_idepth;
1091 if (iSelectedAlbum < 0)
1093 m_musicDatabase.Close();
1098 scraper.LoadAlbumInfo(iSelectedAlbum);
1099 while (!scraper.Completed())
1109 if (scraper.Succeeded())
1111 albumInfo = scraper.GetAlbum(iSelectedAlbum);
1112 album = scraper.GetAlbum(iSelectedAlbum).GetAlbum();
1113 if (result == CNfoFile::COMBINED_NFO)
1114 nfoReader.GetDetails(album,NULL,true);
1115 m_musicDatabase.SetAlbumInfo(params.GetAlbumId(), album, scraper.GetAlbum(iSelectedAlbum).GetSongs(),false);
1119 m_musicDatabase.Close();
1123 // check thumb stuff
1124 GetAlbumArtwork(params.GetAlbumId(), album);
1125 m_musicDatabase.Close();
1129 void CMusicInfoScanner::GetAlbumArtwork(long id, const CAlbum &album)
1131 if (album.thumbURL.m_url.size())
1133 if (m_musicDatabase.GetArtForItem(id, "album", "thumb").empty())
1135 string thumb = CScraperUrl::GetThumbURL(album.thumbURL.GetFirstThumb());
1138 CTextureCache::Get().BackgroundCacheImage(thumb);
1139 m_musicDatabase.SetArtForItem(id, "album", "thumb", thumb);
1145 bool CMusicInfoScanner::DownloadArtistInfo(const CStdString& strPath, const CStdString& strArtist, bool& bCanceled, CGUIDialogProgress* pDialog)
1147 XFILE::MUSICDATABASEDIRECTORY::CQueryParams params;
1148 XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strPath, params);
1151 m_musicDatabase.Open();
1152 if (m_musicDatabase.GetArtistInfo(params.GetArtistId(),artist)) // already got the info
1156 ADDON::ScraperPtr info;
1157 if (!m_musicDatabase.GetScraperForPath(strPath, info, ADDON::ADDON_SCRAPER_ARTISTS) || !info)
1159 m_musicDatabase.Close();
1165 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20320), info->Name().c_str()));
1166 m_handle->SetText(strArtist);
1169 // clear our scraper cache
1172 CMusicInfoScraper scraper(info);
1174 CStdString strArtistPath, strNfo;
1175 m_musicDatabase.GetArtistPath(params.GetArtistId(),strArtistPath);
1176 URIUtils::AddFileToFolder(strArtistPath,"artist.nfo",strNfo);
1177 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1179 if (XFILE::CFile::Exists(strNfo))
1181 CLog::Log(LOGDEBUG,"Found matching nfo file: %s", strNfo.c_str());
1182 result = nfoReader.Create(strNfo, info);
1183 if (result == CNfoFile::FULL_NFO)
1185 CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
1187 nfoReader.GetDetails(artist);
1188 m_musicDatabase.SetArtistInfo(params.GetArtistId(), artist);
1189 map<string, string> artwork = GetArtistArtwork(params.GetArtistId(), &artist);
1190 m_musicDatabase.SetArtForItem(params.GetArtistId(), "artist", artwork);
1191 m_musicDatabase.Close();
1194 else if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
1196 CScraperUrl scrUrl(nfoReader.ScraperUrl());
1197 CMusicArtistInfo artist("nfo",scrUrl);
1198 info = nfoReader.GetScraperInfo();
1199 CLog::Log(LOGDEBUG,"-- nfo-scraper: %s",info->Name().c_str());
1200 CLog::Log(LOGDEBUG,"-- nfo url: %s", scrUrl.m_url[0].m_url.c_str());
1201 scraper.SetScraperInfo(info);
1202 scraper.GetArtists().push_back(artist);
1205 CLog::Log(LOGERROR,"Unable to find an url in nfo file: %s", strNfo.c_str());
1208 if (!scraper.GetArtistCount())
1210 scraper.FindArtistInfo(strArtist);
1212 while (!scraper.Completed())
1223 int iSelectedArtist = 0;
1224 if (result == CNfoFile::NO_NFO)
1226 if (scraper.Succeeded() && scraper.GetArtistCount() >= 1)
1228 // now load the first match
1229 if (pDialog && scraper.GetArtistCount() > 1)
1231 // if we found more then 1 album, let user choose one
1232 CGUIDialogSelect *pDlg = (CGUIDialogSelect*)g_windowManager.GetWindow(WINDOW_DIALOG_SELECT);
1235 pDlg->SetHeading(g_localizeStrings.Get(21890));
1237 pDlg->EnableButton(true, 413); // manual
1239 for (int i = 0; i < scraper.GetArtistCount(); ++i)
1241 // set the label to artist
1242 CFileItem item(scraper.GetArtist(i).GetArtist());
1243 CStdString strTemp=scraper.GetArtist(i).GetArtist().strArtist;
1244 if (!scraper.GetArtist(i).GetArtist().strBorn.IsEmpty())
1245 strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")";
1246 if (!scraper.GetArtist(i).GetArtist().genre.empty())
1248 CStdString genres = StringUtils::Join(scraper.GetArtist(i).GetArtist().genre, g_advancedSettings.m_musicItemSeparator);
1249 if (!genres.empty())
1250 strTemp.Format("[%s] %s", genres.c_str(), strTemp.c_str());
1252 item.SetLabel(strTemp);
1253 item.m_idepth = i; // use this to hold the index of the album in the scraper
1258 // and wait till user selects one
1259 if (pDlg->GetSelectedLabel() < 0)
1261 if (!pDlg->IsButtonPressed())
1266 // manual button pressed
1267 CStdString strNewArtist = strArtist;
1268 if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, g_localizeStrings.Get(16025), false)) return false;
1272 pDialog->SetLine(0, strNewArtist);
1273 pDialog->Progress();
1275 m_musicDatabase.Close();
1276 return DownloadArtistInfo(strPath,strNewArtist,bCanceled,pDialog);
1278 iSelectedArtist = pDlg->GetSelectedItem()->m_idepth;
1284 m_musicDatabase.Close();
1289 scraper.LoadArtistInfo(iSelectedArtist, strArtist);
1290 while (!scraper.Completed())
1300 if (scraper.Succeeded())
1302 artist = scraper.GetArtist(iSelectedArtist).GetArtist();
1303 if (result == CNfoFile::COMBINED_NFO)
1304 nfoReader.GetDetails(artist,NULL,true);
1305 m_musicDatabase.SetArtistInfo(params.GetArtistId(), artist);
1308 // check thumb stuff
1309 map<string, string> artwork = GetArtistArtwork(params.GetArtistId(), &artist);
1310 m_musicDatabase.SetArtForItem(params.GetArtistId(), "artist", artwork);
1312 m_musicDatabase.Close();
1316 map<string, string> CMusicInfoScanner::GetArtistArtwork(long id, const CArtist *artist)
1318 CStdString artistPath;
1319 m_musicDatabase.Open();
1320 bool checkLocal = m_musicDatabase.GetArtistPath(id, artistPath);
1321 m_musicDatabase.Close();
1323 CFileItem item(artistPath, true);
1324 map<string, string> artwork;
1329 thumb = item.GetUserMusicThumb(true);
1330 if (thumb.IsEmpty() && artist)
1331 thumb = CScraperUrl::GetThumbURL(artist->thumbURL.GetFirstThumb());
1332 if (!thumb.IsEmpty())
1334 CTextureCache::Get().BackgroundCacheImage(thumb);
1335 artwork.insert(make_pair("thumb", thumb));
1341 fanart = item.GetLocalFanart();
1342 if (fanart.IsEmpty() && artist)
1343 fanart = artist->fanart.GetImageURL();
1344 if (!fanart.IsEmpty())
1346 CTextureCache::Get().BackgroundCacheImage(fanart);
1347 artwork.insert(make_pair("fanart", fanart));