AnnoucementManager should take a Variant reference instead of pointer
[xbmc:xbmc-antiquated.git] / xbmc / MusicInfoScanner.cpp
1 /*
2  *      Copyright (C) 2005-2008 Team XBMC
3  *      http://www.xbmc.org
4  *
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)
8  *  any later version.
9  *
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.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "MusicInfoScanner.h"
23 #include "MusicDatabase.h"
24 #include "MusicInfoTagLoaderFactory.h"
25 #include "utils/MusicAlbumInfo.h"
26 #include "utils/MusicInfoScraper.h"
27 #include "FileSystem/DirectoryCache.h"
28 #include "FileSystem/MusicDatabaseDirectory.h"
29 #include "FileSystem/MusicDatabaseDirectory/DirectoryNode.h"
30 #include "Util.h"
31 #include "utils/md5.h"
32 #include "utils/GUIInfoManager.h"
33 #include "utils/Variant.h"
34 #include "NfoFile.h"
35 #include "MusicInfoTag.h"
36 #include "GUIWindowManager.h"
37 #include "GUIDialogProgress.h"
38 #include "GUIDialogSelect.h"
39 #include "GUIDialogKeyboard.h"
40 #include "FileSystem/File.h"
41 #include "AdvancedSettings.h"
42 #include "GUISettings.h"
43 #include "Settings.h"
44 #include "FileItem.h"
45 #include "Picture.h"
46 #include "LocalizeStrings.h"
47 #include "StringUtils.h"
48 #include "utils/TimeUtils.h"
49 #include "utils/log.h"
50 #include "utils/AnnouncementManager.h"
51
52 #include <algorithm>
53
54 using namespace std;
55 using namespace MUSIC_INFO;
56 using namespace XFILE;
57 using namespace MUSIC_GRABBER;
58
59 CMusicInfoScanner::CMusicInfoScanner()
60 {
61   m_bRunning = false;
62   m_pObserver = NULL;
63   m_bCanInterrupt = false;
64   m_currentItem=0;
65   m_itemCount=0;
66 }
67
68 CMusicInfoScanner::~CMusicInfoScanner()
69 {
70 }
71
72 void CMusicInfoScanner::Process()
73 {
74   try
75   {
76     unsigned int tick = CTimeUtils::GetTimeMS();
77
78     m_musicDatabase.Open();
79
80     if (m_pObserver)
81       m_pObserver->OnStateChanged(PREPARING);
82
83     m_bCanInterrupt = true;
84
85     CUtil::ThumbCacheClear();
86     g_directoryCache.ClearMusicThumbCache();
87
88     if (m_scanType == 0) // load info from files
89     {
90       CLog::Log(LOGDEBUG, "%s - Starting scan", __FUNCTION__);
91
92       if (m_pObserver)
93         m_pObserver->OnStateChanged(READING_MUSIC_INFO);
94
95       // Reset progress vars
96       m_currentItem=0;
97       m_itemCount=-1;
98
99       // Create the thread to count all files to be scanned
100       SetPriority( GetMinPriority() );
101       CThread fileCountReader(this);
102       if (m_pObserver)
103         fileCountReader.Create();
104
105       // Database operations should not be canceled
106       // using Interupt() while scanning as it could
107       // result in unexpected behaviour.
108       m_bCanInterrupt = false;
109       m_needsCleanup = false;
110
111       bool commit = false;
112       bool cancelled = false;
113       while (!cancelled && m_pathsToScan.size())
114       {
115         /*
116          * A copy of the directory path is used because the path supplied is
117          * immediately removed from the m_pathsToScan set in DoScan(). If the
118          * reference points to the entry in the set a null reference error
119          * occurs.
120          */
121         CStdString directory = *m_pathsToScan.begin();
122         if (!DoScan(directory))
123           cancelled = true;
124         commit = !cancelled;
125       }
126
127       if (commit)
128       {
129         g_infoManager.ResetPersistentCache();
130
131         if (m_needsCleanup)
132         {
133           if (m_pObserver)
134             m_pObserver->OnStateChanged(CLEANING_UP_DATABASE);
135
136           m_musicDatabase.CleanupOrphanedItems();
137
138           if (m_pObserver)
139             m_pObserver->OnStateChanged(COMPRESSING_DATABASE);
140
141           m_musicDatabase.Compress(false);
142         }
143       }
144
145       fileCountReader.StopThread();
146
147       m_musicDatabase.EmptyCache();
148
149       CUtil::ThumbCacheClear();
150       g_directoryCache.ClearMusicThumbCache();
151
152       m_musicDatabase.Close();
153       CLog::Log(LOGDEBUG, "%s - Finished scan", __FUNCTION__);
154
155       tick = CTimeUtils::GetTimeMS() - tick;
156       CLog::Log(LOGNOTICE, "My Music: Scanning for music info using worker thread, operation took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
157     }
158     bool bCanceled;
159     if (m_scanType == 1) // load album info
160     {
161       if (m_pObserver)
162         m_pObserver->OnStateChanged(DOWNLOADING_ALBUM_INFO);
163
164       int iCurrentItem = 1;
165       for (set<CAlbum>::iterator it=m_albumsToScan.begin();it != m_albumsToScan.end();++it)
166       {
167         if (m_pObserver)
168         {
169           m_pObserver->OnDirectoryChanged(it->strArtist+" - "+it->strAlbum);
170           m_pObserver->OnSetProgress(iCurrentItem++, m_albumsToScan.size());
171         }
172
173         CMusicAlbumInfo albumInfo;
174         DownloadAlbumInfo(it->strGenre,it->strArtist,it->strAlbum, bCanceled, albumInfo); // genre field holds path - see fetchalbuminfo()
175
176         if (m_bStop || bCanceled)
177           break;
178       }
179     }
180     if (m_scanType == 2) // load artist info
181     {
182       if (m_pObserver)
183         m_pObserver->OnStateChanged(DOWNLOADING_ARTIST_INFO);
184
185       int iCurrentItem=1;
186       for (set<CArtist>::iterator it=m_artistsToScan.begin();it != m_artistsToScan.end();++it)
187       {
188         if (m_pObserver)
189         {
190           m_pObserver->OnDirectoryChanged(it->strArtist);
191           m_pObserver->OnSetProgress(iCurrentItem++, m_artistsToScan.size());
192         }
193
194         DownloadArtistInfo(it->strGenre,it->strArtist,bCanceled); // genre field holds path - see fetchartistinfo()
195
196         if (m_bStop || bCanceled)
197           break;
198       }
199     }
200
201   }
202   catch (...)
203   {
204     CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning.");
205   }
206   m_bRunning = false;
207   if (m_pObserver)
208     m_pObserver->OnFinished();
209 }
210
211 void CMusicInfoScanner::Start(const CStdString& strDirectory)
212 {
213   m_pathsToScan.clear();
214   m_albumsScanned.clear();
215   m_artistsScanned.clear();
216
217   if (strDirectory.IsEmpty())
218   { // scan all paths in the database.  We do this by scanning all paths in the db, and crossing them off the list as
219     // we go.
220     m_musicDatabase.Open();
221     m_musicDatabase.GetPaths(m_pathsToScan);
222     m_musicDatabase.Close();
223   }
224   else
225     m_pathsToScan.insert(strDirectory);
226   m_pathsToCount = m_pathsToScan;
227   m_scanType = 0;
228   StopThread();
229   Create();
230   m_bRunning = true;
231 }
232
233 void CMusicInfoScanner::FetchAlbumInfo(const CStdString& strDirectory)
234 {
235   m_albumsToScan.clear();
236   m_albumsScanned.clear();
237
238   CFileItemList items;
239   if (strDirectory.IsEmpty())
240   {
241     m_musicDatabase.Open();
242     m_musicDatabase.GetAlbumsNav("musicdb://3/",items,-1,-1,-1,-1);
243     m_musicDatabase.Close();
244   }
245   else
246   {
247     if (CUtil::HasSlashAtEnd(strDirectory)) // directory
248       CDirectory::GetDirectory(strDirectory,items);
249     else
250     {
251       CFileItemPtr item(new CFileItem(strDirectory,false));
252       items.Add(item);
253     }
254   }
255
256   for (int i=0;i<items.Size();++i)
257   {
258     if (CMusicDatabaseDirectory::IsAllItem(items[i]->m_strPath) || items[i]->IsParentFolder())
259       continue;
260
261     CAlbum album;
262     album.strAlbum = items[i]->GetMusicInfoTag()->GetAlbum();
263     album.strArtist = items[i]->GetMusicInfoTag()->GetArtist();
264     album.strGenre = items[i]->m_strPath; // a bit hacky use of field
265     m_albumsToScan.insert(album);
266   }
267
268   m_scanType = 1;
269   StopThread();
270   Create();
271   m_bRunning = true;
272 }
273
274 void CMusicInfoScanner::FetchArtistInfo(const CStdString& strDirectory)
275 {
276   m_artistsToScan.clear();
277   m_artistsScanned.clear();
278   CFileItemList items;
279
280   if (strDirectory.IsEmpty())
281   {
282     m_musicDatabase.Open();
283     m_musicDatabase.GetArtistsNav("musicdb://2/",items,-1,false);
284     m_musicDatabase.Close();
285   }
286   else
287   {
288     if (CUtil::HasSlashAtEnd(strDirectory)) // directory
289       CDirectory::GetDirectory(strDirectory,items);
290     else
291     {
292       CFileItemPtr newItem(new CFileItem(strDirectory,false));
293       items.Add(newItem);
294     }
295   }
296
297   for (int i=0;i<items.Size();++i)
298   {
299     if (CMusicDatabaseDirectory::IsAllItem(items[i]->m_strPath) || items[i]->IsParentFolder())
300       continue;
301
302     CArtist artist;
303     artist.strArtist = items[i]->GetMusicInfoTag()->GetArtist();
304     artist.strGenre = items[i]->m_strPath; // a bit hacky use of field
305     m_artistsToScan.insert(artist);
306   }
307
308   m_scanType = 2;
309   StopThread();
310   Create();
311   m_bRunning = true;
312 }
313
314 bool CMusicInfoScanner::IsScanning()
315 {
316   return m_bRunning;
317 }
318
319 void CMusicInfoScanner::Stop()
320 {
321   if (m_bCanInterrupt)
322     m_musicDatabase.Interupt();
323
324   StopThread();
325 }
326
327 void CMusicInfoScanner::SetObserver(IMusicInfoScannerObserver* pObserver)
328 {
329   m_pObserver = pObserver;
330 }
331
332 bool CMusicInfoScanner::DoScan(const CStdString& strDirectory)
333 {
334   if (m_pObserver)
335     m_pObserver->OnDirectoryChanged(strDirectory);
336
337   /*
338    * remove this path from the list we're processing. This must be done prior to
339    * the check for file or folder exclusion to prevent an infinite while loop
340    * in Process().
341    */
342   set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
343   if (it != m_pathsToScan.end())
344     m_pathsToScan.erase(it);
345
346   // Discard all excluded files defined by m_musicExcludeRegExps
347
348   CStdStringArray regexps = g_advancedSettings.m_audioExcludeFromScanRegExps;
349
350   if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
351     return true;
352
353   // load subfolder
354   CFileItemList items;
355   CDirectory::GetDirectory(strDirectory, items, g_settings.m_musicExtensions + "|.jpg|.tbn|.lrc|.cdg");
356
357   // sort and get the path hash.  Note that we don't filter .cue sheet items here as we want
358   // to detect changes in the .cue sheet as well.  The .cue sheet items only need filtering
359   // if we have a changed hash.
360   items.Sort(SORT_METHOD_LABEL, SORT_ORDER_ASC);
361   CStdString hash;
362   GetPathHash(items, hash);
363
364   // get the folder's thumb (this will cache the album thumb).
365   items.SetMusicThumb(true); // true forces it to get a remote thumb
366
367   // check whether we need to rescan or not
368   CStdString dbHash;
369   if (!m_musicDatabase.GetPathHash(strDirectory, dbHash) || dbHash != hash)
370   { // path has changed - rescan
371     if (dbHash.IsEmpty())
372       CLog::Log(LOGDEBUG, "%s Scanning dir '%s' as not in the database", __FUNCTION__, strDirectory.c_str());
373     else
374       CLog::Log(LOGDEBUG, "%s Rescanning dir '%s' due to change", __FUNCTION__, strDirectory.c_str());
375
376     // filter items in the sub dir (for .cue sheet support)
377     items.FilterCueItems();
378     items.Sort(SORT_METHOD_LABEL, SORT_ORDER_ASC);
379
380     // and then scan in the new information
381     if (RetrieveMusicInfo(items, strDirectory) > 0)
382     {
383       if (m_pObserver)
384         m_pObserver->OnDirectoryScanned(strDirectory);
385     }
386
387     // save information about this folder
388     m_musicDatabase.SetPathHash(strDirectory, hash);
389   }
390   else
391   { // path is the same - no need to rescan
392     CLog::Log(LOGDEBUG, "%s Skipping dir '%s' due to no change", __FUNCTION__, strDirectory.c_str());
393     m_currentItem += CountFiles(items, false);  // false for non-recursive
394
395     // notify our observer of our progress
396     if (m_pObserver)
397     {
398       if (m_itemCount>0)
399         m_pObserver->OnSetProgress(m_currentItem, m_itemCount);
400       m_pObserver->OnDirectoryScanned(strDirectory);
401     }
402   }
403
404   // now scan the subfolders
405   for (int i = 0; i < items.Size(); ++i)
406   {
407     CFileItemPtr pItem = items[i];
408
409     if (m_bStop)
410       break;
411     // if we have a directory item (non-playlist) we then recurse into that folder
412     if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList())
413     {
414       CStdString strPath=pItem->m_strPath;
415       if (!DoScan(strPath))
416       {
417         m_bStop = true;
418       }
419     }
420   }
421
422   return !m_bStop;
423 }
424
425 int CMusicInfoScanner::RetrieveMusicInfo(CFileItemList& items, const CStdString& strDirectory)
426 {
427   CSongMap songsMap;
428
429   // get all information for all files in current directory from database, and remove them
430   if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap))
431     m_needsCleanup = true;
432
433   VECSONGS songsToAdd;
434
435   CStdStringArray regexps = g_advancedSettings.m_audioExcludeFromScanRegExps;
436
437   // for every file found, but skip folder
438   for (int i = 0; i < items.Size(); ++i)
439   {
440     CFileItemPtr pItem = items[i];
441     CStdString strExtension;
442     CUtil::GetExtension(pItem->m_strPath, strExtension);
443
444     if (m_bStop)
445       return 0;
446
447     // Discard all excluded files defined by m_musicExcludeRegExps
448     if (CUtil::ExcludeFileOrFolder(pItem->m_strPath, regexps))
449       continue;
450
451     // dont try reading id3tags for folders, playlists or shoutcast streams
452     if (!pItem->m_bIsFolder && !pItem->IsPlayList() && !pItem->IsPicture() && !pItem->IsLyrics() )
453     {
454       m_currentItem++;
455 //      CLog::Log(LOGDEBUG, "%s - Reading tag for: %s", __FUNCTION__, pItem->m_strPath.c_str());
456
457       // grab info from the song
458       CSong *dbSong = songsMap.Find(pItem->m_strPath);
459
460       CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
461       if (!tag.Loaded() )
462       { // read the tag from a file
463         auto_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(pItem->m_strPath));
464         if (NULL != pLoader.get())
465           pLoader->Load(pItem->m_strPath, tag);
466       }
467
468       // if we have the itemcount, notify our
469       // observer with the progress we made
470       if (m_pObserver && m_itemCount>0)
471         m_pObserver->OnSetProgress(m_currentItem, m_itemCount);
472
473       if (tag.Loaded())
474       {
475         CSong song(tag);
476
477         // ensure our song has a valid filename or else it will assert in AddSong()
478         if (song.strFileName.IsEmpty())
479         {
480           // copy filename from path in case UPnP or other tag loaders didn't specify one (FIXME?)
481           song.strFileName = pItem->m_strPath;
482
483           // if we still don't have a valid filename, skip the song
484           if (song.strFileName.IsEmpty())
485           {
486             // this shouldn't ideally happen!
487             CLog::Log(LOGERROR, "Skipping song since it doesn't seem to have a filename");
488             continue;
489           }
490         }
491
492         song.iStartOffset = pItem->m_lStartOffset;
493         song.iEndOffset = pItem->m_lEndOffset;
494         if (dbSong)
495         { // keep the db-only fields intact on rescan...
496           song.iTimesPlayed = dbSong->iTimesPlayed;
497           song.lastPlayed = dbSong->lastPlayed;
498           song.iKaraokeNumber = dbSong->iKaraokeNumber;
499
500           if (song.rating == '0') song.rating = dbSong->rating;
501         }
502         pItem->SetMusicThumb();
503         song.strThumb = pItem->GetThumbnailImage();
504         songsToAdd.push_back(song);
505 //        CLog::Log(LOGDEBUG, "%s - Tag loaded for: %s", __FUNCTION__, pItem->m_strPath.c_str());
506       }
507       else
508         CLog::Log(LOGDEBUG, "%s - No tag found for: %s", __FUNCTION__, pItem->m_strPath.c_str());
509     }
510   }
511
512   CheckForVariousArtists(songsToAdd);
513   if (!items.HasThumbnail())
514     UpdateFolderThumb(songsToAdd, items.m_strPath);
515
516   // finally, add these to the database
517   set<CStdString> artistsToScan;
518   set< pair<CStdString, CStdString> > albumsToScan;
519   m_musicDatabase.BeginTransaction();
520   for (unsigned int i = 0; i < songsToAdd.size(); ++i)
521   {
522     if (m_bStop)
523     {
524       m_musicDatabase.RollbackTransaction();
525       return i;
526     }
527     CSong &song = songsToAdd[i];
528     m_musicDatabase.AddSong(song, false);
529
530     // Announce the world a new song was added
531     CVariant param;
532     param["musicid"] = (int64_t)song.idSong;
533     ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::Other, "xbmc", "OnNewSong", param);
534
535     artistsToScan.insert(song.strArtist);
536     albumsToScan.insert(make_pair(song.strAlbum, song.strArtist));
537   }
538   m_musicDatabase.CommitTransaction();
539
540   bool bCanceled;
541   for (set<CStdString>::iterator i = artistsToScan.begin(); i != artistsToScan.end(); ++i)
542   {
543     bCanceled = false;
544     long iArtist = m_musicDatabase.GetArtistByName(*i);
545     if (find(m_artistsScanned.begin(),m_artistsScanned.end(),iArtist) == m_artistsScanned.end())
546     {
547       m_artistsScanned.push_back(iArtist);
548       if (!m_bStop && g_guiSettings.GetBool("musiclibrary.downloadinfo"))
549       {
550         CStdString strPath;
551         strPath.Format("musicdb://2/%u/",iArtist);
552         if (!DownloadArtistInfo(strPath,*i, bCanceled)) // assume we want to retry
553           m_artistsScanned.pop_back();
554       }
555       else
556         GetArtistArtwork(iArtist, *i);
557     }
558   }
559
560   if (g_guiSettings.GetBool("musiclibrary.downloadinfo"))
561   {
562     for (set< pair<CStdString, CStdString> >::iterator i = albumsToScan.begin(); i != albumsToScan.end(); ++i)
563     {
564       if (m_bStop)
565         return songsToAdd.size();
566
567       long iAlbum = m_musicDatabase.GetAlbumByName(i->first, i->second);
568       CStdString strPath;
569       strPath.Format("musicdb://3/%u/",iAlbum);
570
571       bCanceled = false;
572       if (find(m_albumsScanned.begin(), m_albumsScanned.end(), iAlbum) == m_albumsScanned.end())
573       {
574         CMusicAlbumInfo albumInfo;
575         if (DownloadAlbumInfo(strPath, i->second, i->first, bCanceled, albumInfo))
576           m_albumsScanned.push_back(iAlbum);
577       }
578     }
579   }
580   if (m_pObserver)
581     m_pObserver->OnStateChanged(READING_MUSIC_INFO);
582
583   return songsToAdd.size();
584 }
585
586 static bool SortSongsByTrack(CSong *song, CSong *song2)
587 {
588   return song->iTrack < song2->iTrack;
589 }
590
591 void CMusicInfoScanner::CheckForVariousArtists(VECSONGS &songsToCheck)
592 {
593   // first, find all the album names for these songs
594   map<CStdString, vector<CSong *> > albumsToAdd;
595   map<CStdString, vector<CSong *> >::iterator it;
596   for (unsigned int i = 0; i < songsToCheck.size(); ++i)
597   {
598     CSong &song = songsToCheck[i];
599     if (!song.strAlbumArtist.IsEmpty()) // albumartist specified, so assume the user knows what they're doing
600       continue;
601     it = albumsToAdd.find(song.strAlbum);
602     if (it == albumsToAdd.end())
603     {
604       vector<CSong *> songs;
605       songs.push_back(&song);
606       albumsToAdd.insert(make_pair(song.strAlbum, songs));
607     }
608     else
609       it->second.push_back(&song);
610   }
611   // as an additional check for those that have multiple albums in the same folder, ignore albums
612   // that have overlapping track numbers
613   for (it = albumsToAdd.begin(); it != albumsToAdd.end();)
614   {
615     vector<CSong *> &songs = it->second;
616     bool overlappingTrackNumbers(false);
617     if (songs.size() > 1)
618     {
619       sort(songs.begin(), songs.end(), SortSongsByTrack);
620       for (unsigned int i = 0; i < songs.size() - 1; i++)
621       {
622         CSong *song = songs[i];
623         CSong *song2 = songs[i+1];
624         if (song->iTrack == song2->iTrack)
625         {
626           overlappingTrackNumbers = true;
627           break;
628         }
629       }
630     }
631     if (overlappingTrackNumbers)
632     { // remove this album
633       albumsToAdd.erase(it++);
634     }
635     else
636       it++;
637   }
638
639   // ok, now run through these albums, and check whether they qualify as a "various artist" album
640   // an album is considered a various artists album if the songs' primary artist differs
641   // it qualifies as a "single artist with featured artists" album if the primary artist is the same, but secondary artists differ
642   for (it = albumsToAdd.begin(); it != albumsToAdd.end(); it++)
643   {
644     const CStdString &album = it->first;
645     vector<CSong *> &songs = it->second;
646     if (!album.IsEmpty() && songs.size() > 1)
647     {
648       bool variousArtists(false);
649       bool singleArtistWithFeaturedArtists(false);
650       for (unsigned int i = 0; i < songs.size() - 1; i++)
651       {
652         CSong *song1 = songs[i];
653         CSong *song2 = songs[i+1];
654         CStdStringArray vecArtists1, vecArtists2;
655         StringUtils::SplitString(song1->strArtist, g_advancedSettings.m_musicItemSeparator, vecArtists1);
656         StringUtils::SplitString(song2->strArtist, g_advancedSettings.m_musicItemSeparator, vecArtists2);
657         CStdString primaryArtist1 = vecArtists1[0]; primaryArtist1.TrimRight();
658         CStdString primaryArtist2 = vecArtists2[0]; primaryArtist2.TrimRight();
659         if (primaryArtist1 != primaryArtist2)
660         { // primary artist differs -> a various artists album
661           variousArtists = true;
662           break;
663         }
664         else if (song1->strArtist != song2->strArtist)
665         { // have more than one artist, the first artist(s) agree, but the full artist name doesn't
666           // so this is likely a single-artist compilation (ie with other artists featured on some tracks) album
667           singleArtistWithFeaturedArtists = true;
668         }
669       }
670       if (variousArtists)
671       { // have a various artists album - update all songs to be the various artist
672         for (unsigned int i = 0; i < songs.size(); i++)
673         {
674           CSong *song = songs[i];
675           song->strAlbumArtist = g_localizeStrings.Get(340); // Various Artists
676         }
677       }
678       else if (singleArtistWithFeaturedArtists)
679       { // have an album where all the first artists agree - make this the album artist
680         CStdStringArray vecArtists;
681         StringUtils::SplitString(songs[0]->strArtist, g_advancedSettings.m_musicItemSeparator, vecArtists);
682         CStdString albumArtist(vecArtists[0]);
683         for (unsigned int i = 0; i < songs.size(); i++)
684         {
685           CSong *song = songs[i];
686           song->strAlbumArtist = albumArtist; // first artist of all tracks
687         }
688       }
689     }
690   }
691 }
692
693 bool CMusicInfoScanner::HasSingleAlbum(const VECSONGS &songs, CStdString &album, CStdString &artist)
694 {
695   // check how many unique albums are in this path, and if there's only one, and it has a thumb
696   // then cache the thumb as the folder thumb
697   for (unsigned int i = 0; i < songs.size(); i++)
698   {
699     const CSong &song = songs[i];
700     // don't bother with empty album tags - they're treated as singles, and there's no way to determine
701     // whether more than one track in the folder is supposed to mean they belong to an "album"
702     if (song.strAlbum.IsEmpty())
703       return false;
704
705     CStdString albumArtist = song.strAlbumArtist.IsEmpty() ? song.strArtist : song.strAlbumArtist;
706
707     if (!album.IsEmpty() && (album != song.strAlbum || artist != albumArtist))
708       return false; // have more than one album
709
710     album = song.strAlbum;
711     artist = albumArtist;
712   }
713   return !album.IsEmpty();
714 }
715
716 void CMusicInfoScanner::UpdateFolderThumb(const VECSONGS &songs, const CStdString &folderPath)
717 {
718   CStdString album, artist;
719   if (!HasSingleAlbum(songs, album, artist)) return;
720   // Was the album art of this album read during scan?
721   CStdString albumCoverArt(CUtil::GetCachedAlbumThumb(album, artist));
722   if (CUtil::ThumbExists(albumCoverArt))
723   {
724     CStdString folderPath1(folderPath);
725     // Folder art is cached without the slash at end
726     CUtil::RemoveSlashAtEnd(folderPath1);
727     CStdString folderCoverArt(CUtil::GetCachedMusicThumb(folderPath1));
728     // copy as directory thumb as well
729     if (CFile::Cache(albumCoverArt, folderCoverArt))
730       CUtil::ThumbCacheAdd(folderCoverArt, true);
731   }
732 }
733
734 // This function is run by another thread
735 void CMusicInfoScanner::Run()
736 {
737   int count = 0;
738   while (!m_bStop && m_pathsToCount.size())
739     count+=CountFilesRecursively(*m_pathsToCount.begin());
740   m_itemCount = count;
741 }
742
743 // Recurse through all folders we scan and count files
744 int CMusicInfoScanner::CountFilesRecursively(const CStdString& strPath)
745 {
746   // load subfolder
747   CFileItemList items;
748 //  CLog::Log(LOGDEBUG, __FUNCTION__" - processing dir: %s", strPath.c_str());
749   CDirectory::GetDirectory(strPath, items, g_settings.m_musicExtensions, false);
750
751   if (m_bStop)
752     return 0;
753
754   // true for recursive counting
755   int count = CountFiles(items, true);
756
757   // remove this path from the list we're processing
758   set<CStdString>::iterator it = m_pathsToCount.find(strPath);
759   if (it != m_pathsToCount.end())
760     m_pathsToCount.erase(it);
761
762 //  CLog::Log(LOGDEBUG, __FUNCTION__" - finished processing dir: %s", strPath.c_str());
763   return count;
764 }
765
766 int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive)
767 {
768   int count = 0;
769   for (int i=0; i<items.Size(); ++i)
770   {
771     const CFileItemPtr pItem=items[i];
772
773     if (recursive && pItem->m_bIsFolder)
774       count+=CountFilesRecursively(pItem->m_strPath);
775     else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
776       count++;
777   }
778   return count;
779 }
780
781 int CMusicInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
782 {
783   // Create a hash based on the filenames, filesize and filedate.  Also count the number of files
784   if (0 == items.Size()) return 0;
785   XBMC::XBMC_MD5 md5state;
786   int count = 0;
787   for (int i = 0; i < items.Size(); ++i)
788   {
789     const CFileItemPtr pItem = items[i];
790     md5state.append(pItem->m_strPath);
791     md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
792     FILETIME time = pItem->m_dateTime;
793     md5state.append((unsigned char *)&time, sizeof(FILETIME));
794     if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
795       count++;
796   }
797   md5state.getDigest(hash);
798   return count;
799 }
800
801 #define THRESHOLD .95f
802
803 bool CMusicInfoScanner::DownloadAlbumInfo(const CStdString& strPath, const CStdString& strArtist, const CStdString& strAlbum, bool& bCanceled, CMusicAlbumInfo& albumInfo, CGUIDialogProgress* pDialog)
804 {
805   CAlbum album;
806   VECSONGS songs;
807   XFILE::MUSICDATABASEDIRECTORY::CQueryParams params;
808   XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strPath, params);
809   bCanceled = false;
810   m_musicDatabase.Open();
811   if (m_musicDatabase.HasAlbumInfo(params.GetAlbumId()) && m_musicDatabase.GetAlbumInfo(params.GetAlbumId(),album,&songs))
812     return true;
813
814   // find album info
815   ADDON::ScraperPtr info;
816   if (!m_musicDatabase.GetScraperForPath(strPath, info, ADDON::ADDON_SCRAPER_ALBUMS) || !info)
817   {
818     m_musicDatabase.Close();
819     return false;
820   }
821
822   if (m_pObserver)
823   {
824     m_pObserver->OnStateChanged(DOWNLOADING_ALBUM_INFO);
825     m_pObserver->OnDirectoryChanged(strAlbum);
826   }
827
828   // clear our scraper cache
829   info->ClearCache();
830
831   CMusicInfoScraper scraper(info);
832
833   // handle nfo files
834   CStdString strAlbumPath, strNfo;
835   m_musicDatabase.GetAlbumPath(params.GetAlbumId(),strAlbumPath);
836   CUtil::AddFileToFolder(strAlbumPath,"album.nfo",strNfo);
837   CNfoFile::NFOResult result=CNfoFile::NO_NFO;
838   CNfoFile nfoReader;
839   if (XFILE::CFile::Exists(strNfo))
840   {
841     CLog::Log(LOGDEBUG,"Found matching nfo file: %s", strNfo.c_str());
842     result = nfoReader.Create(strNfo, info, -1, strPath);
843     if (result == CNfoFile::FULL_NFO)
844     {
845       CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
846       CAlbum album;
847       nfoReader.GetDetails(album);
848       m_musicDatabase.SetAlbumInfo(params.GetAlbumId(), album, album.songs);
849       GetAlbumArtwork(params.GetAlbumId(), album);
850       m_musicDatabase.Close();
851       return true;
852     }
853     else if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
854     {
855       CScraperUrl scrUrl(nfoReader.m_strImDbUrl);
856       CMusicAlbumInfo album("nfo",scrUrl);
857       info = nfoReader.GetScraperInfo();
858       CLog::Log(LOGDEBUG,"-- nfo-scraper: %s",info->Name().c_str());
859       CLog::Log(LOGDEBUG,"-- nfo url: %s", scrUrl.m_url[0].m_url.c_str());
860       scraper.SetScraperInfo(info);
861       scraper.GetAlbums().push_back(album);
862     }
863     else
864       CLog::Log(LOGERROR,"Unable to find an url in nfo file: %s", strNfo.c_str());
865   }
866
867   if (!scraper.CheckValidOrFallback(g_guiSettings.GetString("musiclibrary.albumsscraper")))
868   { // the current scraper is invalid, as is the default - bail
869     CLog::Log(LOGERROR, "%s - current and default scrapers are invalid.  Pick another one", __FUNCTION__);
870     return false;
871   }
872
873   if (!scraper.GetAlbumCount())
874     scraper.FindAlbumInfo(strAlbum, strArtist);
875
876   while (!scraper.Completed())
877   {
878     if (m_bStop)
879     {
880       scraper.Cancel();
881       bCanceled = true;
882     }
883     Sleep(1);
884   }
885
886   CGUIDialogSelect *pDlg=NULL;
887   int iSelectedAlbum=0;
888   if (result == CNfoFile::NO_NFO)
889   {
890     iSelectedAlbum = -1; // set negative so that we can detect a failure
891     if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1)
892     {
893       int bestMatch = -1;
894       double bestRelevance = 0;
895       double minRelevance = THRESHOLD;
896       if (scraper.GetAlbumCount() > 1) // score the matches
897       {
898         //show dialog with all albums found
899         if (pDialog)
900         {
901           pDlg = (CGUIDialogSelect*)g_windowManager.GetWindow(WINDOW_DIALOG_SELECT);
902           pDlg->SetHeading(g_localizeStrings.Get(181).c_str());
903           pDlg->Reset();
904           pDlg->EnableButton(true, 413); // manual
905         }
906
907         for (int i = 0; i < scraper.GetAlbumCount(); ++i)
908         {
909           CMusicAlbumInfo& info = scraper.GetAlbum(i);
910           double relevance = info.GetRelevance();
911           if (relevance < 0)
912             relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, strAlbum, info.GetAlbum().strArtist, strArtist);
913
914           // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
915           // otherwise, perfect matches only
916           if (relevance >= max(minRelevance, bestRelevance))
917           { // we auto-select the best of these
918             bestRelevance = relevance;
919             bestMatch = i;
920           }
921           if (pDialog)
922           {
923             // set the label to [relevance]  album - artist
924             CStdString strTemp;
925             strTemp.Format("[%0.2f]  %s", relevance, info.GetTitle2());
926             CFileItem item(strTemp);
927             item.m_idepth = i; // use this to hold the index of the album in the scraper
928             pDlg->Add(&item);
929           }
930           if (relevance > .99f) // we're so close, no reason to search further
931             break;
932         }
933       }
934       else
935       {
936         CMusicAlbumInfo& info = scraper.GetAlbum(0);
937         double relevance = info.GetRelevance();
938         if (relevance < 0)
939           relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, strAlbum, info.GetAlbum().strArtist, strArtist);
940         if (relevance < THRESHOLD)
941         {
942           m_musicDatabase.Close();
943           return false;
944         }
945         bestRelevance = relevance;
946         bestMatch = 0;
947       }
948
949       iSelectedAlbum = bestMatch;
950       if (pDialog && bestRelevance < THRESHOLD)
951       {
952         pDlg->Sort(false);
953         pDlg->DoModal();
954
955         // and wait till user selects one
956         if (pDlg->GetSelectedLabel() < 0)
957         { // none chosen
958           if (!pDlg->IsButtonPressed())
959           {
960             bCanceled = true;
961             return false;
962           }
963           // manual button pressed
964           CStdString strNewAlbum = strAlbum;
965           if (!CGUIDialogKeyboard::ShowAndGetInput(strNewAlbum, g_localizeStrings.Get(16011), false)) return false;
966           if (strNewAlbum == "") return false;
967
968           CStdString strNewArtist = strArtist;
969           if (!CGUIDialogKeyboard::ShowAndGetInput(strNewArtist, g_localizeStrings.Get(16025), false)) return false;
970
971           pDialog->SetLine(0, strNewAlbum);
972           pDialog->SetLine(1, strNewArtist);
973           pDialog->Progress();
974
975           m_musicDatabase.Close();
976           return DownloadAlbumInfo(strPath,strNewArtist,strNewAlbum,bCanceled,albumInfo,pDialog);
977         }
978         iSelectedAlbum = pDlg->GetSelectedItem().m_idepth;
979       }
980     }
981
982     if (iSelectedAlbum < 0)
983     {
984       m_musicDatabase.Close();
985       return false;
986     }
987   }
988
989   scraper.LoadAlbumInfo(iSelectedAlbum);
990   while (!scraper.Completed())
991   {
992     if (m_bStop)
993     {
994       bCanceled = true;
995       scraper.Cancel();
996     }
997     Sleep(1);
998   }
999
1000   if (scraper.Succeeded())
1001   {
1002     albumInfo = scraper.GetAlbum(iSelectedAlbum);
1003     album = scraper.GetAlbum(iSelectedAlbum).GetAlbum();
1004     if (result == CNfoFile::COMBINED_NFO)
1005       nfoReader.GetDetails(album);
1006     m_musicDatabase.SetAlbumInfo(params.GetAlbumId(), album, scraper.GetAlbum(iSelectedAlbum).GetSongs(),false);
1007   }
1008   else
1009   {
1010     m_musicDatabase.Close();
1011     return false;
1012   }
1013
1014   // check thumb stuff
1015   GetAlbumArtwork(params.GetAlbumId(), album);
1016   m_musicDatabase.Close();
1017   return true;
1018 }
1019
1020 void CMusicInfoScanner::GetAlbumArtwork(long id, const CAlbum &album)
1021 {
1022   if (album.thumbURL.m_url.size())
1023   {
1024     CStdString thumb;
1025     if (!m_musicDatabase.GetAlbumThumb(id, thumb) || thumb.IsEmpty() || !XFILE::CFile::Exists(thumb))
1026     {
1027       thumb = CUtil::GetCachedAlbumThumb(album.strAlbum,album.strArtist);
1028       CScraperUrl::DownloadThumbnail(thumb,album.thumbURL.m_url[0]);
1029       m_musicDatabase.SaveAlbumThumb(id, thumb);
1030     }
1031   }
1032 }
1033
1034 bool CMusicInfoScanner::DownloadArtistInfo(const CStdString& strPath, const CStdString& strArtist, bool& bCanceled, CGUIDialogProgress* pDialog)
1035 {
1036   XFILE::MUSICDATABASEDIRECTORY::CQueryParams params;
1037   XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strPath, params);
1038   bCanceled = false;
1039   CArtist artist;
1040   m_musicDatabase.Open();
1041   if (m_musicDatabase.GetArtistInfo(params.GetArtistId(),artist)) // already got the info
1042     return true;
1043
1044   // find artist info
1045   ADDON::ScraperPtr info;
1046   if (!m_musicDatabase.GetScraperForPath(strPath, info, ADDON::ADDON_SCRAPER_ARTISTS) || !info)
1047   {
1048     m_musicDatabase.Close();
1049     return false;
1050   }
1051
1052   // clear our scraper cache
1053   info->ClearCache();
1054
1055   if (m_pObserver)
1056   {
1057     m_pObserver->OnStateChanged(DOWNLOADING_ARTIST_INFO);
1058     m_pObserver->OnDirectoryChanged(strArtist);
1059   }
1060
1061   CMusicInfoScraper scraper(info);
1062   // handle nfo files
1063   CStdString strArtistPath, strNfo;
1064   m_musicDatabase.GetArtistPath(params.GetArtistId(),strArtistPath);
1065   CUtil::AddFileToFolder(strArtistPath,"artist.nfo",strNfo);
1066   CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1067   CNfoFile nfoReader;
1068   if (XFILE::CFile::Exists(strNfo))
1069   {
1070     CLog::Log(LOGDEBUG,"Found matching nfo file: %s", strNfo.c_str());
1071     result = nfoReader.Create(strNfo, info);
1072     if (result == CNfoFile::FULL_NFO)
1073     {
1074       CLog::Log(LOGDEBUG, "%s Got details from nfo", __FUNCTION__);
1075       CArtist artist;
1076       nfoReader.GetDetails(artist);
1077       m_musicDatabase.SetArtistInfo(params.GetArtistId(), artist);
1078       GetArtistArtwork(params.GetArtistId(), strArtist, &artist);
1079       m_musicDatabase.Close();
1080       return true;
1081     }
1082     else if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
1083     {
1084       CScraperUrl scrUrl(nfoReader.m_strImDbUrl);
1085       CMusicArtistInfo artist("nfo",scrUrl);
1086       info = nfoReader.GetScraperInfo();
1087       CLog::Log(LOGDEBUG,"-- nfo-scraper: %s",info->Name().c_str());
1088       CLog::Log(LOGDEBUG,"-- nfo url: %s", scrUrl.m_url[0].m_url.c_str());
1089       scraper.SetScraperInfo(info);
1090       scraper.GetArtists().push_back(artist);
1091     }
1092     else
1093       CLog::Log(LOGERROR,"Unable to find an url in nfo file: %s", strNfo.c_str());
1094   }
1095
1096   if (!scraper.GetArtistCount())
1097     scraper.FindArtistInfo(strArtist);
1098
1099   while (!scraper.Completed())
1100   {
1101     if (m_bStop)
1102     {
1103       scraper.Cancel();
1104       bCanceled = true;
1105     }
1106     Sleep(1);
1107   }
1108
1109   int iSelectedArtist = 0;
1110   if (result == CNfoFile::NO_NFO)
1111   {
1112     if (scraper.Succeeded() && scraper.GetArtistCount() >= 1)
1113     {
1114       // now load the first match
1115       if (pDialog && scraper.GetArtistCount() > 1)
1116       {
1117         // if we found more then 1 album, let user choose one
1118         CGUIDialogSelect *pDlg = (CGUIDialogSelect*)g_windowManager.GetWindow(WINDOW_DIALOG_SELECT);
1119         if (pDlg)
1120         {
1121           pDlg->SetHeading(g_localizeStrings.Get(21890));
1122           pDlg->Reset();
1123           pDlg->EnableButton(true, 413); // manual
1124
1125           for (int i = 0; i < scraper.GetArtistCount(); ++i)
1126           {
1127             // set the label to artist
1128             CFileItem item(scraper.GetArtist(i).GetArtist());
1129             CStdString strTemp=scraper.GetArtist(i).GetArtist().strArtist;
1130             if (!scraper.GetArtist(i).GetArtist().strBorn.IsEmpty())
1131               strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")";
1132             if (!scraper.GetArtist(i).GetArtist().strGenre.IsEmpty())
1133               strTemp.Format("[%s] %s",scraper.GetArtist(i).GetArtist().strGenre.c_str(),strTemp.c_str());
1134             item.SetLabel(strTemp);
1135             item.m_idepth = i; // use this to hold the index of the album in the scraper
1136             pDlg->Add(&item);
1137           }
1138           pDlg->DoModal();
1139
1140           // and wait till user selects one
1141           if (pDlg->GetSelectedLabel() < 0)
1142           { // none chosen
1143             if (!pDlg->IsButtonPressed())
1144             {
1145               bCanceled = true;
1146               return false;
1147             }
1148             // manual button pressed
1149             CStdString strNewArtist = strArtist;
1150             if (!CGUIDialogKeyboard::ShowAndGetInput(strNewArtist, g_localizeStrings.Get(16025), false)) return false;
1151
1152             if (pDialog)
1153             {
1154               pDialog->SetLine(0, strNewArtist);
1155               pDialog->Progress();
1156             }
1157             m_musicDatabase.Close();
1158             return DownloadArtistInfo(strPath,strNewArtist,bCanceled,pDialog);
1159           }
1160           iSelectedArtist = pDlg->GetSelectedItem().m_idepth;
1161         }
1162       }
1163     }
1164     else
1165     {
1166       m_musicDatabase.Close();
1167       return false;
1168     }
1169   }
1170
1171   scraper.GetArtist(iSelectedArtist).m_strSearch = strArtist;
1172   CUtil::URLEncode(scraper.GetArtist(iSelectedArtist).m_strSearch);
1173   scraper.LoadArtistInfo(iSelectedArtist);
1174
1175   while (!scraper.Completed())
1176   {
1177     if (m_bStop)
1178     {
1179       scraper.Cancel();
1180       bCanceled = true;
1181     }
1182     Sleep(1);
1183   }
1184
1185   if (scraper.Succeeded())
1186   {
1187     artist = scraper.GetArtist(iSelectedArtist).GetArtist();
1188     if (result == CNfoFile::COMBINED_NFO)
1189       nfoReader.GetDetails(artist);
1190     m_musicDatabase.SetArtistInfo(params.GetArtistId(), artist);
1191   }
1192
1193   // check thumb stuff
1194   GetArtistArtwork(params.GetArtistId(), strArtist, &artist);
1195
1196   m_musicDatabase.Close();
1197   return true;
1198 }
1199
1200 void CMusicInfoScanner::GetArtistArtwork(long id, const CStdString &artistName, const CArtist *artist)
1201 {
1202   CStdString artistPath;
1203   CFileItem item(artistName);
1204   CStdString thumb = item.GetCachedArtistThumb();
1205   if (m_musicDatabase.GetArtistPath(id, artistPath) && !XFILE::CFile::Exists(thumb))
1206   {
1207     CStdString localThumb = CUtil::AddFileToFolder(artistPath, "folder.jpg");
1208     if (XFILE::CFile::Exists(localThumb))
1209       CPicture::CreateThumbnail(localThumb, thumb);
1210   }
1211   if (!XFILE::CFile::Exists(thumb) && artist && artist->thumbURL.m_url.size())
1212     CScraperUrl::DownloadThumbnail(thumb, artist->thumbURL.m_url[0]);
1213
1214   // check fanart
1215   CFileItem item2(artistPath, true);
1216   item2.GetMusicInfoTag()->SetArtist(artistName);
1217   CStdString cachedImage = item2.GetCachedFanart();
1218   if (!CFile::Exists(cachedImage))
1219   { // check for local fanart
1220     CLog::Log(LOGDEBUG, "%s looking for fanart for artist %s in folder %s", __FUNCTION__, artistName.c_str(), item2.m_strPath.c_str());
1221     if (!item2.CacheLocalFanart())
1222     {
1223       CLog::Log(LOGDEBUG, "%s no local fanart found for artist %s", __FUNCTION__, artistName.c_str());
1224       if (artist && !artist->fanart.m_xml.IsEmpty() && !artist->fanart.DownloadImage(item2.GetCachedFanart()))
1225         CLog::Log(LOGERROR, "Failed to download fanart %s to %s", artist->fanart.GetImageURL().c_str(), item2.GetCachedFanart().c_str());
1226     }
1227   }
1228 }