void CMusicInfoLoader::OnLoaderFinish() { // cleanup last loaded songs from database m_songsMap.Clear(); // cleanup cache loaded from HD m_mapFileItems->Clear(); if (!m_bStop) { // check for art VECSONGS songs; songs.reserve(m_pVecItems->Size()); for (int i = 0; i < m_pVecItems->Size(); ++i) { CFileItemPtr pItem = m_pVecItems->Get(i); if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsNFO() || pItem->IsInternetStream()) continue; if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->Loaded()) { CSong song(*pItem->GetMusicInfoTag()); song.strThumb = pItem->GetArt("thumb"); song.idSong = i; // for the lookup below songs.push_back(song); } } VECALBUMS albums; CMusicInfoScanner::CategoriseAlbums(songs, albums); CMusicInfoScanner::FindArtForAlbums(albums, m_pVecItems->GetPath()); for (VECALBUMS::iterator i = albums.begin(); i != albums.end(); ++i) { string albumArt = i->art["thumb"]; for (VECSONGS::iterator j = i->songs.begin(); j != i->songs.end(); ++j) { if (!j->strThumb.empty()) m_pVecItems->Get(j->idSong)->SetArt("thumb", j->strThumb); else m_pVecItems->Get(j->idSong)->SetArt("thumb", albumArt); } } } // Save loaded items to HD if (!m_strCacheFileName.IsEmpty()) SaveCache(m_strCacheFileName, *m_pVecItems); else if (!m_bStop && (m_databaseHits > 1 || m_tagReads > 0)) m_pVecItems->Save(); m_musicDatabase.Close(); }
void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const CStdString &path) { /* If there's a single album in the folder, then art can be taken from the folder art. */ std::string albumArt; if (albums.size() == 1) { CFileItem album(path, true); albumArt = album.GetUserMusicThumb(true); if (!albumArt.empty()) albums[0].art["thumb"] = albumArt; } for (VECALBUMS::iterator i = albums.begin(); i != albums.end(); ++i) { CAlbum &album = *i; if (albums.size() != 1) albumArt = ""; /* Find art that is common across these items If we find a single art image we treat it as the album art and discard song art else we use first as album art and keep everything as song art. */ bool singleArt = true; CSong *art = NULL; for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k) { CSong &song = *k; if (song.HasArt()) { if (art && !art->ArtMatches(song)) { singleArt = false; break; } if (!art) art = &song; } } /* assign the first art found to the album - better than no art at all */ if (art && albumArt.empty()) { if (!art->strThumb.empty()) albumArt = art->strThumb; else albumArt = CTextureCache::GetWrappedImageURL(art->strFileName, "music"); } if (!albumArt.empty()) album.art["thumb"] = albumArt; if (singleArt) { //if singleArt then we can clear the artwork for all songs for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k) k->strThumb.clear(); } else { // more than one piece of art was found for these songs, so cache per song for (VECSONGS::iterator k = album.songs.begin(); k != album.songs.end(); ++k) { if (k->strThumb.empty() && !k->embeddedArt.empty()) k->strThumb = CTextureCache::GetWrappedImageURL(k->strFileName, "music"); } } } if (albums.size() == 1 && !albumArt.empty()) { // assign to folder thumb as well CMusicThumbLoader::SetCachedImage(path, "thumb", albumArt); } }
int CMusicInfoScanner::RetrieveMusicInfo(CFileItemList& items, const CStdString& strDirectory) { CSongMap songsMap; // get all information for all files in current directory from database, and remove them if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap)) m_needsCleanup = true; VECSONGS songsToAdd; CStdStringArray regexps = g_advancedSettings.m_audioExcludeFromScanRegExps; // for every file found, but skip folder for (int i = 0; i < items.Size(); ++i) { CFileItemPtr pItem = items[i]; CStdString strExtension; URIUtils::GetExtension(pItem->GetPath(), strExtension); if (m_bStop) return 0; // Discard all excluded files defined by m_musicExcludeRegExps if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps)) continue; // dont try reading id3tags for folders, playlists or shoutcast streams if (!pItem->m_bIsFolder && !pItem->IsPlayList() && !pItem->IsPicture() && !pItem->IsLyrics() ) { m_currentItem++; // CLog::Log(LOGDEBUG, "%s - Reading tag for: %s", __FUNCTION__, pItem->GetPath().c_str()); // grab info from the song CSong *dbSong = songsMap.Find(pItem->GetPath()); CMusicInfoTag& tag = *pItem->GetMusicInfoTag(); if (!tag.Loaded() ) { // read the tag from a file auto_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(pItem->GetPath())); if (NULL != pLoader.get()) pLoader->Load(pItem->GetPath(), tag); } // if we have the itemcount, update our // dialog with the progress we made if (m_handle && m_itemCount>0) m_handle->SetPercentage(m_currentItem/(float)m_itemCount*100); if (tag.Loaded()) { CSong song(tag); // ensure our song has a valid filename or else it will assert in AddSong() if (song.strFileName.IsEmpty()) { // copy filename from path in case UPnP or other tag loaders didn't specify one (FIXME?) song.strFileName = pItem->GetPath(); // if we still don't have a valid filename, skip the song if (song.strFileName.IsEmpty()) { // this shouldn't ideally happen! CLog::Log(LOGERROR, "Skipping song since it doesn't seem to have a filename"); continue; } } song.iStartOffset = pItem->m_lStartOffset; song.iEndOffset = pItem->m_lEndOffset; song.strThumb = pItem->GetUserMusicThumb(true); if (dbSong) { // keep the db-only fields intact on rescan... song.iTimesPlayed = dbSong->iTimesPlayed; song.lastPlayed = dbSong->lastPlayed; song.iKaraokeNumber = dbSong->iKaraokeNumber; if (song.rating == '0') song.rating = dbSong->rating; if (song.strThumb.empty()) song.strThumb = dbSong->strThumb; } songsToAdd.push_back(song); // CLog::Log(LOGDEBUG, "%s - Tag loaded for: %s", __FUNCTION__, pItem->GetPath().c_str()); } else CLog::Log(LOGDEBUG, "%s - No tag found for: %s", __FUNCTION__, pItem->GetPath().c_str()); } } VECALBUMS albums; CategoriseAlbums(songsToAdd, albums); FindArtForAlbums(albums, items.GetPath()); // finally, add these to the database m_musicDatabase.BeginTransaction(); int numAdded = 0; set<int> albumsToScan; set<int> artistsToScan; for (VECALBUMS::iterator i = albums.begin(); i != albums.end(); ++i) { vector<int> songIDs; int idAlbum = m_musicDatabase.AddAlbum(*i, songIDs); numAdded += i->songs.size(); if (m_bStop) { m_musicDatabase.RollbackTransaction(); return numAdded; } // Build the artist & album sets albumsToScan.insert(idAlbum); for (vector<int>::iterator j = songIDs.begin(); j != songIDs.end(); ++j) { vector<int> songArtists; m_musicDatabase.GetArtistsBySong(*j, false, songArtists); artistsToScan.insert(songArtists.begin(), songArtists.end()); } std::vector<int> albumArtists; m_musicDatabase.GetArtistsByAlbum(idAlbum, false, albumArtists); artistsToScan.insert(albumArtists.begin(), albumArtists.end()); } m_musicDatabase.CommitTransaction(); // Download info & artwork bool bCanceled; for (set<int>::iterator it = artistsToScan.begin(); it != artistsToScan.end(); ++it) { bCanceled = false; if (find(m_artistsScanned.begin(),m_artistsScanned.end(), *it) == m_artistsScanned.end()) { CStdString strArtist = m_musicDatabase.GetArtistById(*it); m_artistsScanned.push_back(*it); if (!m_bStop && (m_flags & SCAN_ONLINE)) { CStdString strPath; strPath.Format("musicdb://2/%u/", *it); if (!DownloadArtistInfo(strPath, strArtist, bCanceled)) // assume we want to retry m_artistsScanned.pop_back(); } else { map<string, string> artwork = GetArtistArtwork(*it); m_musicDatabase.SetArtForItem(*it, "artist", artwork); } } } if (m_flags & SCAN_ONLINE) { for (set<int>::iterator it = albumsToScan.begin(); it != albumsToScan.end(); ++it) { if (m_bStop) return songsToAdd.size(); CStdString strPath; strPath.Format("musicdb://3/%u/",*it); CAlbum album; m_musicDatabase.GetAlbumInfo(*it, album, NULL); bCanceled = false; if (find(m_albumsScanned.begin(), m_albumsScanned.end(), *it) == m_albumsScanned.end()) { CMusicAlbumInfo albumInfo; if (DownloadAlbumInfo(strPath, StringUtils::Join(album.artist, g_advancedSettings.m_musicItemSeparator), album.strAlbum, bCanceled, albumInfo)) m_albumsScanned.push_back(*it); } } } if (m_handle) m_handle->SetTitle(g_localizeStrings.Get(505)); return songsToAdd.size(); }
int CMusicInfoScanner::RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items) { MAPSONGS songsMap; // get all information for all files in current directory from database, and remove them if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap)) m_needsCleanup = true; CFileItemList scannedItems; if (ScanTags(items, scannedItems) == INFO_CANCELLED || scannedItems.Size() == 0) return 0; VECALBUMS albums; FileItemsToAlbums(scannedItems, albums, &songsMap); FindArtForAlbums(albums, items.GetPath()); int numAdded = 0; ADDON::AddonPtr addon; ADDON::ScraperPtr albumScraper; ADDON::ScraperPtr artistScraper; if(ADDON::CAddonMgr::Get().GetDefault(ADDON::ADDON_SCRAPER_ALBUMS, addon)) albumScraper = boost::dynamic_pointer_cast<ADDON::CScraper>(addon); if(ADDON::CAddonMgr::Get().GetDefault(ADDON::ADDON_SCRAPER_ARTISTS, addon)) artistScraper = boost::dynamic_pointer_cast<ADDON::CScraper>(addon); // Add each album for (VECALBUMS::iterator album = albums.begin(); album != albums.end(); ++album) { if (m_bStop) break; album->strPath = strDirectory; m_musicDatabase.AddAlbum(*album); // Yuk - this is a kludgy way to do what we want to do, but it will work to sort // out artist fanart until we can restructure the artist fanart to work more // like the album fanart. This has to be done after we've added the album so // we have the artist IDs to update, but before we call UpdateDatabaseArtistInfo. if (albums.size() == 1 && album->artistCredits.size() > 0 && !StringUtils::EqualsNoCase(album->artistCredits[0].GetArtist(), "various artists") && !StringUtils::EqualsNoCase(album->artistCredits[0].GetArtist(), "various")) { CArtist artist; if (m_musicDatabase.GetArtist(album->artistCredits[0].GetArtistId(), artist)) { artist.strPath = URIUtils::GetParentPath(strDirectory); m_musicDatabase.SetArtForItem(artist.idArtist, MediaTypeArtist, GetArtistArtwork(artist)); } } if ((m_flags & SCAN_ONLINE)) { if (!albumScraper || !artistScraper) continue; INFO_RET albumScrapeStatus = INFO_NOT_FOUND; if (!m_musicDatabase.HasAlbumBeenScraped(album->idAlbum)) albumScrapeStatus = UpdateDatabaseAlbumInfo(*album, albumScraper, false); if (albumScrapeStatus == INFO_ADDED) { for (VECARTISTCREDITS::const_iterator artistCredit = album->artistCredits.begin(); artistCredit != album->artistCredits.end(); ++artistCredit) { if (m_bStop) break; if (!m_musicDatabase.HasArtistBeenScraped(artistCredit->GetArtistId())) { CArtist artist; m_musicDatabase.GetArtist(artistCredit->GetArtistId(), artist); UpdateDatabaseArtistInfo(artist, artistScraper, false); } } for (VECSONGS::iterator song = album->songs.begin(); song != album->songs.end(); song++) { if (m_bStop) break; for (VECARTISTCREDITS::const_iterator artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end(); ++artistCredit) { if (m_bStop) break; CMusicArtistInfo musicArtistInfo; if (!m_musicDatabase.HasArtistBeenScraped(artistCredit->GetArtistId())) { CArtist artist; m_musicDatabase.GetArtist(artistCredit->GetArtistId(), artist); UpdateDatabaseArtistInfo(artist, artistScraper, false); } } } } } numAdded += album->songs.size(); } if (m_handle) m_handle->SetTitle(g_localizeStrings.Get(505)); return numAdded; }
void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */) { /* * Step 1: Convert the FileItems into Songs. * If they're MB tagged, create albums directly from the FileItems. * If they're non-MB tagged, index them by album name ready for step 2. */ map<string, VECSONGS> songsByAlbumNames; for (int i = 0; i < items.Size(); ++i) { CMusicInfoTag& tag = *items[i]->GetMusicInfoTag(); CSong song(*items[i]); // keep the db-only fields intact on rescan... if (songsMap != NULL) { MAPSONGS::iterator it = songsMap->find(items[i]->GetPath()); if (it != songsMap->end()) { song.iTimesPlayed = it->second.iTimesPlayed; song.lastPlayed = it->second.lastPlayed; song.iKaraokeNumber = it->second.iKaraokeNumber; if (song.rating == '0') song.rating = it->second.rating; if (song.strThumb.empty()) song.strThumb = it->second.strThumb; } } if (!tag.GetMusicBrainzAlbumID().empty()) { VECALBUMS::iterator it; for (it = albums.begin(); it != albums.end(); ++it) if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID()) break; if (it == albums.end()) { CAlbum album(*items[i]); album.songs.push_back(song); albums.push_back(album); } else it->songs.push_back(song); } else songsByAlbumNames[tag.GetAlbum()].push_back(song); } /* Step 2: Split into unique albums based on album name and album artist In the case where the album artist is unknown, we use the primary artist (i.e. first artist from each song). */ for (map<string, VECSONGS>::iterator songsByAlbumName = songsByAlbumNames.begin(); songsByAlbumName != songsByAlbumNames.end(); ++songsByAlbumName) { VECSONGS &songs = songsByAlbumName->second; // sort the songs by tracknumber to identify duplicate track numbers sort(songs.begin(), songs.end(), SortSongsByTrack); // map the songs to their primary artists bool tracksOverlap = false; bool hasAlbumArtist = false; bool isCompilation = true; map<string, vector<CSong *> > artists; for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song) { // test for song overlap if (song != songs.begin() && song->iTrack == (song - 1)->iTrack) tracksOverlap = true; if (!song->bCompilation) isCompilation = false; // get primary artist string primary; if (!song->albumArtist.empty()) { primary = song->albumArtist[0]; hasAlbumArtist = true; } else if (!song->artist.empty()) primary = song->artist[0]; // add to the artist map artists[primary].push_back(&(*song)); } /* We have a compilation if 1. album name is non-empty AND 2a. no tracks overlap OR 2b. all tracks are marked as part of compilation AND 3a. a unique primary artist is specified as "various" or "various artists" OR 3b. we have at least two primary artists and no album artist specified. */ bool compilation = !songsByAlbumName->first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a if (artists.size() == 1) { string artist = artists.begin()->first; StringUtils::ToLower(artist); if (!StringUtils::EqualsNoCase(artist, "various") && !StringUtils::EqualsNoCase(artist, "various artists")) // 3a compilation = false; } else if (hasAlbumArtist) // 3b compilation = false; if (compilation) { CLog::Log(LOGDEBUG, "Album '%s' is a compilation as there's no overlapping tracks and %s", songsByAlbumName->first.c_str(), hasAlbumArtist ? "the album artist is 'Various'" : "there is more than one unique artist"); artists.clear(); std::string various = g_localizeStrings.Get(340); // Various Artists vector<string> va; va.push_back(various); for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song) { song->albumArtist = va; artists[various].push_back(&(*song)); } } /* Step 3: Find the common albumartist for each song and assign albumartist to those tracks that don't have it set. */ for (map<string, vector<CSong *> >::iterator j = artists.begin(); j != artists.end(); ++j) { // find the common artist for these songs vector<CSong *> &artistSongs = j->second; vector<string> common = artistSongs.front()->albumArtist.empty() ? artistSongs.front()->artist : artistSongs.front()->albumArtist; for (vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k) { unsigned int match = 0; vector<string> &compare = (*k)->albumArtist.empty() ? (*k)->artist : (*k)->albumArtist; for (; match < common.size() && match < compare.size(); match++) { if (compare[match] != common[match]) break; } common.erase(common.begin() + match, common.end()); } /* Step 4: Assign the album artist for each song that doesn't have it set and add to the album vector */ CAlbum album; album.strAlbum = songsByAlbumName->first; album.artist = common; for (vector<string>::iterator it = common.begin(); it != common.end(); ++it) { std::string strJoinPhrase = (it == --common.end() ? "" : g_advancedSettings.m_musicItemSeparator); CArtistCredit artistCredit(*it, strJoinPhrase); album.artistCredits.push_back(artistCredit); } album.bCompilation = compilation; for (vector<CSong *>::iterator k = artistSongs.begin(); k != artistSongs.end(); ++k) { if ((*k)->albumArtist.empty()) (*k)->albumArtist = common; // TODO: in future we may wish to union up the genres, for now we assume they're the same album.genre = (*k)->genre; // in addition, we may want to use year as discriminating for albums album.iYear = (*k)->iYear; album.songs.push_back(**k); } albums.push_back(album); } } }
void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */) { /* * Step 1: Convert the FileItems into Songs. * If they're MB tagged, create albums directly from the FileItems. * If they're non-MB tagged, index them by album name ready for step 2. */ std::map<std::string, VECSONGS> songsByAlbumNames; for (int i = 0; i < items.Size(); ++i) { CMusicInfoTag& tag = *items[i]->GetMusicInfoTag(); CSong song(*items[i]); // keep the db-only fields intact on rescan... if (songsMap != NULL) { MAPSONGS::iterator it = songsMap->find(items[i]->GetPath()); if (it != songsMap->end()) { song.iTimesPlayed = it->second.iTimesPlayed; song.lastPlayed = it->second.lastPlayed; if (song.rating == 0) song.rating = it->second.rating; if (song.userrating == 0) song.userrating = it->second.userrating; if (song.strThumb.empty()) song.strThumb = it->second.strThumb; } } if (!tag.GetMusicBrainzAlbumID().empty()) { VECALBUMS::iterator it; for (it = albums.begin(); it != albums.end(); ++it) if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID()) break; if (it == albums.end()) { CAlbum album(*items[i]); album.songs.push_back(song); albums.push_back(album); } else it->songs.push_back(song); } else songsByAlbumNames[tag.GetAlbum()].push_back(song); } /* Step 2: Split into unique albums based on album name and album artist In the case where the album artist is unknown, we use the primary artist (i.e. first artist from each song). */ for (std::map<std::string, VECSONGS>::iterator songsByAlbumName = songsByAlbumNames.begin(); songsByAlbumName != songsByAlbumNames.end(); ++songsByAlbumName) { VECSONGS &songs = songsByAlbumName->second; // sort the songs by tracknumber to identify duplicate track numbers sort(songs.begin(), songs.end(), SortSongsByTrack); // map the songs to their primary artists bool tracksOverlap = false; bool hasAlbumArtist = false; bool isCompilation = true; std::map<std::string, std::vector<CSong *> > artists; for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song) { // test for song overlap if (song != songs.begin() && song->iTrack == (song - 1)->iTrack) tracksOverlap = true; if (!song->bCompilation) isCompilation = false; // get primary artist std::string primary; if (!song->GetAlbumArtist().empty()) { primary = song->GetAlbumArtist()[0]; hasAlbumArtist = true; } else if (!song->artistCredits.empty()) primary = song->artistCredits.begin()->GetArtist(); // add to the artist map artists[primary].push_back(&(*song)); } /* We have a Various Artists compilation if 1. album name is non-empty AND 2a. no tracks overlap OR 2b. all tracks are marked as part of compilation AND 3a. a unique primary artist is specified as "various", "various artists" or the localized value OR 3b. we have at least two primary artists and no album artist specified. */ std::string various = g_localizeStrings.Get(340); // Various Artists bool compilation = !songsByAlbumName->first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a if (artists.size() == 1) { std::string artist = artists.begin()->first; StringUtils::ToLower(artist); if (!StringUtils::EqualsNoCase(artist, "various") && !StringUtils::EqualsNoCase(artist, "various artists") && !StringUtils::EqualsNoCase(artist, various)) // 3a compilation = false; } else if (hasAlbumArtist) // 3b compilation = false; //Such a compilation album is stored with the localized value for "various artists" as the album artist if (compilation) { CLog::Log(LOGDEBUG, "Album '%s' is a compilation as there's no overlapping tracks and %s", songsByAlbumName->first.c_str(), hasAlbumArtist ? "the album artist is 'Various'" : "there is more than one unique artist"); artists.clear(); std::vector<std::string> va; va.push_back(various); for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song) { song->SetAlbumArtist(va); artists[various].push_back(&(*song)); } } /* We also have a compilation album when album name is non-empty and ALL tracks are marked as part of a compilation even if an album artist is given, or all songs have the same primary artist. For example an anthology - a collection of recordings from various old sources combined together such as a "best of", retrospective or rarities type release. Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b. The album artist can be determined just like any normal album. */ if (!compilation && !songsByAlbumName->first.empty() && isCompilation) { compilation = true; CLog::Log(LOGDEBUG, "Album '%s' is a compilation as all songs are marked as part of a compilation", songsByAlbumName->first.c_str()); } /* Step 3: Find the common albumartist for each song and assign albumartist to those tracks that don't have it set. */ for (std::map<std::string, std::vector<CSong *> >::iterator j = artists.begin(); j != artists.end(); ++j) { // find the common artist for these songs std::vector<CSong *> &artistSongs = j->second; std::vector<std::string> common = artistSongs.front()->GetAlbumArtist().empty() ? artistSongs.front()->GetArtist() : artistSongs.front()->GetAlbumArtist(); for (std::vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k) { unsigned int match = 0; std::vector<std::string> compare = (*k)->GetAlbumArtist().empty() ? (*k)->GetArtist() : (*k)->GetAlbumArtist(); for (; match < common.size() && match < compare.size(); match++) { if (compare[match] != common[match]) break; } common.erase(common.begin() + match, common.end()); } /* Step 4: Assign the album artist for each song that doesn't have it set and add to the album vector */ CAlbum album; album.strAlbum = songsByAlbumName->first; for (std::vector<std::string>::iterator it = common.begin(); it != common.end(); ++it) { album.artistCredits.emplace_back(StringUtils::Trim(*it)); } album.bCompilation = compilation; for (std::vector<CSong *>::iterator k = artistSongs.begin(); k != artistSongs.end(); ++k) { if ((*k)->GetAlbumArtist().empty()) (*k)->SetAlbumArtist(common); //! @todo in future we may wish to union up the genres, for now we assume they're the same album.genre = (*k)->genre; // in addition, we may want to use year as discriminating for albums album.iYear = (*k)->iYear; album.strLabel = (*k)->strRecordLabel; album.strType = (*k)->strAlbumType; album.songs.push_back(**k); } albums.push_back(album); } } }